@sap/cds-compiler
Advanced tools
Comparing version 2.15.4 to 3.0.0
@@ -22,3 +22,3 @@ #!/usr/bin/env node | ||
const { compactModel } = require('../lib/json/to-csn'); | ||
const { toRenameWithCsn, alterConstraintsWithCsn } = require('../lib/backends'); | ||
const { toRename: _toRename } = require('../lib/render/toRename'); | ||
const util = require('util'); | ||
@@ -37,2 +37,3 @@ const fs = require('fs'); | ||
const { availableBetaFlags } = require('../lib/base/model'); | ||
const { alterConstraintsWithCsn } = require('../lib/render/manageConstraints'); | ||
@@ -61,7 +62,7 @@ // Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode), | ||
function remapCmdOptions(options, cmdOptions) { | ||
if (!cmdOptions) | ||
return options; | ||
function remapCmdOptions(options, command) { | ||
if (!command || !options[command]) | ||
return; | ||
for (const [ key, value ] of Object.entries(cmdOptions)) { | ||
for (const [ key, value ] of Object.entries(options[command])) { | ||
switch (key) { | ||
@@ -94,2 +95,5 @@ case 'names': | ||
break; | ||
case 'flavor': | ||
options.csnFlavor = value; | ||
break; | ||
default: | ||
@@ -99,3 +103,3 @@ options[key] = value; | ||
} | ||
return options; | ||
delete options[command]; | ||
} | ||
@@ -281,2 +285,3 @@ | ||
remapCmdOptions( options, command ); | ||
@@ -305,3 +310,3 @@ if (commandsWithoutCompilation[command]) { | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const cdlResult = main.to.cdl(csn, remapCmdOptions(options)); | ||
const cdlResult = main.to.cdl(csn, options); | ||
for (const name in cdlResult) | ||
@@ -331,7 +336,7 @@ writeToFileOrDisplay(options.out, `${name}.cds`, cdlResult[name]); | ||
if (options.toHana && options.toHana.csn) { | ||
displayNamedCsn(for_hdbcds(csn, remapCmdOptions(options, options.toHana)), 'hana_csn'); | ||
if (options.csn) { | ||
displayNamedCsn(for_hdbcds(csn, options), 'hana_csn'); | ||
} | ||
else { | ||
const hanaResult = main.to.hdbcds(csn, remapCmdOptions(options, options.toHana)); | ||
const hanaResult = main.to.hdbcds(csn, options); | ||
for (const name in hanaResult) | ||
@@ -347,16 +352,14 @@ writeToFileOrDisplay(options.out, name, hanaResult[name]); | ||
function toOdata( model ) { | ||
if (options.toOdata && | ||
options.toOdata.version === 'v4x') { | ||
options.toOdata.version = 'v4'; | ||
options.toOdata.odataFormat = 'structured'; | ||
options.toOdata.odataContainment = true; | ||
if (options.odataVersion === 'v4x') { | ||
options.odataVersion = 'v4'; | ||
options.odataFormat = 'structured'; | ||
options.odataContainment = true; | ||
} | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const odataOptions = remapCmdOptions(options, options.toOdata); | ||
if (options.toOdata && options.toOdata.csn) { | ||
const odataCsn = main.for.odata(csn, odataOptions); | ||
if (options.csn) { | ||
const odataCsn = main.for.odata(csn, options); | ||
displayNamedCsn(odataCsn, 'odata_csn'); | ||
} | ||
else if (options.toOdata && options.toOdata.json) { | ||
const result = main.to.edm.all(csn, odataOptions); | ||
else if (options.json) { | ||
const result = main.to.edm.all(csn, options); | ||
for (const serviceName in result) | ||
@@ -366,3 +369,3 @@ writeToFileOrDisplay(options.out, `${serviceName}.json`, result[serviceName]); | ||
else { | ||
const result = main.to.edmx.all(csn, odataOptions); | ||
const result = main.to.edmx.all(csn, options); | ||
for (const serviceName in result) | ||
@@ -381,4 +384,4 @@ writeToFileOrDisplay(options.out, `${serviceName}.xml`, result[serviceName]); | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const renameResult = toRenameWithCsn(csn, options); | ||
let storedProcedure = `PROCEDURE RENAME_${renameResult.options.toRename.names.toUpperCase()}_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n`; | ||
const renameResult = _toRename(csn, options); | ||
let storedProcedure = `PROCEDURE RENAME_${renameResult.options.sqlMapping.toUpperCase()}_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n`; | ||
for (const name in renameResult.rename) { | ||
@@ -389,3 +392,3 @@ storedProcedure += ` --\n -- ${name}\n --\n`; | ||
storedProcedure += 'END;\n'; | ||
writeToFileOrDisplay(options.out, `storedProcedure_${renameResult.options.toRename.names}_to_plain.sql`, storedProcedure, true); | ||
writeToFileOrDisplay(options.out, `storedProcedure_${renameResult.options.sqlMapping}_to_plain.sql`, storedProcedure, true); | ||
return model; | ||
@@ -412,8 +415,8 @@ } | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
if (options.toSql && options.toSql.src === 'hdi') { | ||
if (options.toSql.csn) { | ||
displayNamedCsn(for_hdi(csn, remapCmdOptions(options, options.toSql)), 'hdi_csn'); | ||
if (options.src === 'hdi') { | ||
if (options.csn) { | ||
displayNamedCsn(for_hdi(csn, options), 'hdi_csn'); | ||
} | ||
else { | ||
const hdiResult = main.to.hdi(csn, remapCmdOptions(options, options.toSql)); | ||
const hdiResult = main.to.hdi(csn, options); | ||
for (const name in hdiResult) | ||
@@ -423,7 +426,7 @@ writeToFileOrDisplay(options.out, name, hdiResult[name]); | ||
} | ||
else if (options.toSql && options.toSql.csn) { | ||
displayNamedCsn(for_sql(csn, remapCmdOptions(options, options.toSql)), 'sql_csn'); | ||
else if (options.csn) { | ||
displayNamedCsn(for_sql(csn, options), 'sql_csn'); | ||
} | ||
else { | ||
const sqlResult = main.to.sql(csn, remapCmdOptions(options, options.toSql)); | ||
const sqlResult = main.to.sql(csn, options); | ||
writeToFileOrDisplay(options.out, 'model.sql', sqlResult.join('\n'), true); | ||
@@ -541,3 +544,3 @@ } | ||
const csn = compactModel(xsn, options); | ||
if (command === 'toCsn' && options.toCsn && options.toCsn.withLocalized) | ||
if (command === 'toCsn' && options.withLocalized) | ||
addLocalizationViews(csn, options); | ||
@@ -561,3 +564,3 @@ if (options.enrichCsn) | ||
else if (!options.lintMode && !options.internalMsg) { | ||
if (command === 'toCsn' && options.toCsn && options.toCsn.withLocalized) | ||
if (command === 'toCsn' && options.withLocalized) | ||
addLocalizationViews(csn, options); | ||
@@ -564,0 +567,0 @@ writeToFileOrDisplay(options.out, `${name}.json`, csn, true); |
@@ -11,8 +11,7 @@ # ChangeLog of Beta Features for cdx compiler and backends | ||
## Version 2.XX.YY | ||
## Version 3.0.0 - 2022-XX-YY | ||
### Removed `assocsWithParams` | ||
### Removed `addTextsLanguageAssoc` | ||
Instead, of using the beta flag `assocsWithParams`, you can change the severity of the messages | ||
`def-unexpected-paramview-assoc` and `def-unexpected-calcview-assoc`. | ||
Instead, use the option `addTextsLanguageAssoc`, which is available since v2.8.0. | ||
@@ -19,0 +18,0 @@ ## Version 2.12.0 - 2022-01-25 |
@@ -14,4 +14,38 @@ # ChangeLog of deprecated Features for cdx compiler and backends | ||
## Version 2.XX.YY - 2022-MM-DD | ||
## Version 3.0.0 - 2022-XX-YY | ||
Version 3 of the cds-compiler removes all v2 deprecated flags. | ||
### Removed `createLocalizedViews` | ||
### Removed `downgradableErrors` | ||
### Removed `generatedEntityNameWithUnderscore` | ||
### Removed `longAutoexposed` | ||
### Removed `noElementsExpansion` | ||
### Removed `noInheritedAutoexposeViaComposition` | ||
### Removed `noScopedRedirections` | ||
### Removed `oldVirtualNotNullPropagation` | ||
### Removed `parensAsStrings` | ||
### Removed `projectionAsQuery` | ||
### Removed `redirectInSubQueries` | ||
### Removed `renderVirtualElements` | ||
### Removed `shortAutoexposed` | ||
### Removed `unmanagedUpInComponent` | ||
### Removed `v1KeysForTemporal` | ||
## Version 2.13.0 - 2022-03-22 | ||
### Added `redirectInSubQueries` | ||
@@ -18,0 +52,0 @@ |
@@ -8,2 +8,13 @@ # Versioning | ||
<!-- toc: start --> | ||
1. [Public API](#public-api) | ||
2. [Patch Versions](#patch-versions) | ||
3. [Minor Versions](#minor-versions) | ||
4. [Beta Flags](#beta-flags) | ||
5. [Deprecated Flags](#deprecated-flags) | ||
6. [Command Line Tool `cdsc`](#command-line-tool-cdsc) | ||
<!-- toc: end --> | ||
## Public API | ||
@@ -45,3 +56,3 @@ | ||
The compiler provides so called “beta flags” that enable or disable certain | ||
The compiler provides so-called “beta flags” that enable or disable certain | ||
features. We do not guarantee that any such flags stay consistent between | ||
@@ -62,3 +73,11 @@ patch versions! Beta flags may change any time. | ||
## Command Line Tool `cdsc` | ||
`bin/cdsc.js` as well as all other command line tools do _not_ guarantee any | ||
stability. It is considered a compiler internal tool that only serves for | ||
debugging. The official command line tool `cds` of the `@sap/cds` and | ||
`@sap/cds-dk` packages are to be used by users. That means commands and | ||
options may change any time without prior notice. Changes may still be listed | ||
in [CHANGELOG.md](../CHANGELOG.md). | ||
[SemVer]: https://semver.org/ | ||
@@ -65,0 +84,0 @@ [§1]: https://semver.org/#spec-item-1 |
{ | ||
"root": true, | ||
"env": { | ||
"es6": true, | ||
"es2020": true, | ||
"node": true | ||
@@ -10,3 +10,3 @@ }, | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"ecmaVersion": 2020, | ||
"sourceType": "script" | ||
@@ -13,0 +13,0 @@ }, |
@@ -5,16 +5,16 @@ /** @module API */ | ||
const prepareOptions = require('./options'); | ||
const backends = require('../backends'); | ||
const { setProp } = require('../base/model'); | ||
const { emptyLocation } = require('../base/location'); | ||
const { CompilationError, makeMessageFunction } = require('../base/messages'); | ||
const { recompileX } = require('../compiler/index'); | ||
const { compactModel, sortCsn } = require('../json/to-csn'); | ||
const { transform4odataWithCsn } = require('../transform/forOdataNew.js'); | ||
const { toSqlDdl } = require('../render/toSql'); | ||
const { compareModels } = require('../modelCompare/compare'); | ||
const sortViews = require('../model/sortViews'); | ||
const { getResultingName } = require('../model/csnUtils'); | ||
const { timetrace } = require('../utils/timetrace'); | ||
const { transformForHanaWithCsn } = require('../transform/forHanaNew'); | ||
const prepareOptions = lazyload('./options'); | ||
const baseModel = lazyload('../base/model'); | ||
const location = lazyload('../base/location'); | ||
const messages = lazyload('../base/messages'); | ||
const compiler = lazyload('../compiler/index'); | ||
const toCsn = lazyload('../json/to-csn'); | ||
const forOdataNew = lazyload('../transform/forOdataNew.js'); | ||
const toSql = lazyload('../render/toSql'); | ||
const toCdl = require('../render/toCdl'); | ||
const modelCompare = lazyload('../modelCompare/compare'); | ||
const sortViews = lazyload('../model/sortViews'); | ||
const csnUtils = lazyload('../model/csnUtils'); | ||
const timetrace = lazyload('../utils/timetrace'); | ||
const forHanaNew = lazyload('../transform/forHanaNew'); | ||
@@ -30,13 +30,11 @@ /** | ||
function getFileName(artifactName, csn) { | ||
return getResultingName(csn, 'quoted', artifactName); | ||
return csnUtils.getResultingName(csn, 'quoted', artifactName); | ||
} | ||
const propertyToCheck = { | ||
odata: 'toOdata', | ||
}; | ||
const { cloneCsnNonDict } = require('../model/csnUtils'); | ||
const { toHdbcdsSource } = require('../render/toHdbcds'); | ||
const { ModelError } = require('../base/error'); | ||
const { forEach } = require('../utils/objectUtils'); | ||
const { forEach, forEachKey } = require('../utils/objectUtils'); | ||
const { checkRemovedDeprecatedFlags } = require('../base/model'); | ||
const { csn2edm, csn2edmAll } = require('../edm/csn2edm'); | ||
@@ -59,11 +57,10 @@ const relevantGeneralOptions = [ /* for future generic options */ ]; | ||
const relevant = {}; | ||
const propName = propertyToCheck[transformation]; | ||
for (const name of relevantOptionNames ) { | ||
if (options[propName][name] !== undefined) | ||
relevant[name] = options[propName][name]; | ||
if (options[name] !== undefined) | ||
relevant[name] = options[name]; | ||
} | ||
for (const name of optionalOptionNames ) { | ||
if (options[propName][name] !== undefined) | ||
relevant[name] = options[propName][name]; | ||
if (options[name] !== undefined) | ||
relevant[name] = options[name]; | ||
} | ||
@@ -76,6 +73,6 @@ | ||
if (!csn.meta) | ||
setProp(csn, 'meta', {}); | ||
baseModel.setProp(csn, 'meta', {}); | ||
setProp(csn.meta, 'options', relevant); | ||
setProp(csn.meta, 'transformation', transformation); | ||
baseModel.setProp(csn.meta, 'options', relevant); | ||
baseModel.setProp(csn.meta, 'transformation', transformation); | ||
} | ||
@@ -99,3 +96,3 @@ | ||
} | ||
const { error, warning, throwWithAnyError } = makeMessageFunction(csn, options, module); | ||
const { error, warning, throwWithAnyError } = messages.makeMessageFunction(csn, options, module); | ||
@@ -136,3 +133,3 @@ for (const name of relevantOptionNames ) { | ||
function odataInternal(csn, internalOptions) { | ||
const oDataCsn = transform4odataWithCsn(csn, internalOptions); | ||
const oDataCsn = forOdataNew.transform4odataWithCsn(csn, internalOptions); | ||
attachTransformerCharacteristics(oDataCsn, 'odata', internalOptions, relevantOdataOptions, warnAboutMismatchOdata); | ||
@@ -159,9 +156,9 @@ return oDataCsn; | ||
* @param {object} [externalOptions={}] Options | ||
* @returns {CDL} { <artifactName>: <CDL representation>, ...} | ||
* @returns {object} { model: string, namespace: string, unappliedExtensions: string } | ||
*/ | ||
function cdl(csn, externalOptions = {}) { | ||
const internalOptions = prepareOptions.to.cdl(externalOptions); | ||
const { result } = backends.toCdlWithCsn(cloneCsnNonDict(csn, internalOptions), internalOptions); | ||
return result; | ||
return toCdl.csnToCdl(cloneCsnNonDict(csn, internalOptions), internalOptions); | ||
} | ||
/** | ||
@@ -178,4 +175,5 @@ * Transform a CSN like to.sql | ||
internalOptions.transformation = 'sql'; | ||
internalOptions.toSql.csn = true; | ||
return backends.toSqlWithCsn(csn, internalOptions).csn; | ||
const transformedCsn = forHanaNew.transformForHanaWithCsn(csn, internalOptions, 'to.sql'); | ||
return internalOptions.testMode ? toCsn.sortCsn(transformedCsn, internalOptions) : transformedCsn; | ||
} | ||
@@ -192,4 +190,6 @@ /** | ||
const internalOptions = prepareOptions.to.hdi(options); | ||
internalOptions.toSql.csn = true; | ||
return backends.toSqlWithCsn(csn, internalOptions).csn; | ||
internalOptions.transformation = 'sql'; | ||
const transformedCsn = forHanaNew.transformForHanaWithCsn(csn, internalOptions, 'to.hdi'); | ||
return internalOptions.testMode ? toCsn.sortCsn(transformedCsn, internalOptions) : transformedCsn; | ||
} | ||
@@ -208,5 +208,5 @@ /** | ||
const hanaCsn = transformForHanaWithCsn(csn, internalOptions, 'to.hdbcds'); | ||
const hanaCsn = forHanaNew.transformForHanaWithCsn(csn, internalOptions, 'to.hdbcds'); | ||
return internalOptions.testMode ? sortCsn(hanaCsn, internalOptions) : hanaCsn; | ||
return internalOptions.testMode ? toCsn.sortCsn(hanaCsn, internalOptions) : hanaCsn; | ||
} | ||
@@ -225,9 +225,13 @@ | ||
const { error } = messages.makeMessageFunction(csn, internalOptions, 'for.odata'); | ||
if (internalOptions.sqlDialect === 'postgres' && !baseModel.isBetaEnabled(internalOptions, 'postgres')) | ||
error(null, null, 'sqlDialect: \'postgres\' is only supported with beta-flag \'postgres\''); | ||
// we need the CSN for view sorting | ||
internalOptions.toSql.csn = true; | ||
const transformedCsn = forSql(csn, options); | ||
const sqls = toSql.toSqlDdl(transformedCsn, internalOptions); | ||
const intermediateResult = backends.toSqlWithCsn(csn, internalOptions); | ||
const result = sortViews({ csn: transformedCsn, sql: sqls.sql }); | ||
const result = sortViews(intermediateResult); | ||
return result.map(obj => obj.sql).filter(create => create); | ||
@@ -247,9 +251,5 @@ } | ||
// we need the CSN for view sorting | ||
internalOptions.toSql.csn = true; | ||
const sqlCSN = forHdi(csn, options); | ||
const sqls = toSql.toSqlDdl(sqlCSN, internalOptions); | ||
const intermediateResult = backends.toSqlWithCsn(csn, internalOptions); | ||
const sqlCSN = intermediateResult.csn; | ||
delete intermediateResult.csn; | ||
if (internalOptions.testMode) { | ||
@@ -261,3 +261,3 @@ // All this mapping is needed because sortViews crossmatches | ||
const flat = flattenResultStructure(intermediateResult); | ||
const flat = flattenResultStructure(sqls); | ||
@@ -293,3 +293,3 @@ const nameMapping = Object.create(null); | ||
return remapNames(flattenResultStructure(intermediateResult), sqlCSN, k => !k.endsWith('.hdbindex')); | ||
return remapNames(flattenResultStructure(sqls), sqlCSN, k => !k.endsWith('.hdbindex')); | ||
} | ||
@@ -360,41 +360,13 @@ /** | ||
function hdiMigration(csn, options, beforeImage) { | ||
/** | ||
* Swap arguments in case of inverted argument order. | ||
* This is for backward compatibility with @sap/cds@4.5.(2…3). | ||
* | ||
* @todo Remove in cds-compiler@2.x | ||
* @param {HdiOptions|CSN.Model} inputOptions Options or CSN image | ||
* @param {HdiOptions|CSN.Model} inputBeforeImage CSN image or options | ||
* @returns {Array} Array where the real options come first | ||
*/ | ||
function backwardCompatible(inputOptions, inputBeforeImage) { | ||
/** | ||
* Check whether the given argument is a CSN | ||
* | ||
* @param {object} arg Argument to verify | ||
* @returns {boolean} True if it is a CSN | ||
*/ | ||
function isBeforeImage(arg) { | ||
return arg === null || [ 'definitions', 'meta', '$version' ].some(key => key in arg); | ||
} | ||
return isBeforeImage(inputBeforeImage) | ||
? [ inputOptions, inputBeforeImage ] | ||
: [ inputBeforeImage, inputOptions ]; | ||
} | ||
[ options, beforeImage ] = backwardCompatible(options, beforeImage); | ||
const internalOptions = prepareOptions.to.hdi(options); | ||
internalOptions.toSql.csn = true; | ||
// Prepare after-image. | ||
// FIXME: Is this needed? | ||
// cloneCsnMessages(csn, options, internalOptions); | ||
const afterImage = backends.toSqlWithCsn(csn, internalOptions).csn; | ||
const afterImage = forHdi(csn, options); | ||
// Compare both images. | ||
const diff = compareModels(beforeImage || afterImage, afterImage, internalOptions); | ||
const diff = modelCompare.compareModels(beforeImage || afterImage, afterImage, internalOptions); | ||
// Convert the diff to SQL. | ||
internalOptions.forHana = true; // Make it pass the SQL rendering | ||
const { deletions, migrations, ...hdbkinds } = toSqlDdl(diff, internalOptions); | ||
const { deletions, migrations, ...hdbkinds } = toSql.toSqlDdl(diff, internalOptions); | ||
@@ -458,3 +430,3 @@ return { | ||
function hdbcds(csn, options = {}) { | ||
timetrace.start('to.hdbcds'); | ||
timetrace.timetrace.start('to.hdbcds'); | ||
const internalOptions = prepareOptions.to.hdbcds(options); | ||
@@ -466,3 +438,3 @@ internalOptions.transformation = 'hdbcds'; | ||
const result = flattenResultStructure(toHdbcdsSource(hanaCsn, internalOptions)); | ||
timetrace.stop(); | ||
timetrace.timetrace.stop(); | ||
return result; | ||
@@ -489,7 +461,7 @@ } | ||
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata'); | ||
servicesEdmj = backends.preparedCsnToEdm(csn, service, internalOptions); | ||
servicesEdmj = preparedCsnToEdm(csn, service, internalOptions); | ||
} | ||
else { | ||
const oDataCsn = odataInternal(csn, internalOptions); | ||
servicesEdmj = backends.preparedCsnToEdm(oDataCsn, service, internalOptions); | ||
servicesEdmj = preparedCsnToEdm(oDataCsn, service, internalOptions); | ||
} | ||
@@ -510,5 +482,5 @@ return servicesEdmj.edmj; | ||
const internalOptions = prepareOptions.to.edm(options); | ||
const { error } = makeMessageFunction(csn, internalOptions, 'for.odata'); | ||
const { error } = messages.makeMessageFunction(csn, internalOptions, 'for.odata'); | ||
if (internalOptions.version === 'v2') | ||
if (internalOptions.odataVersion === 'v2') | ||
error(null, null, 'OData JSON output is not available for OData V2'); | ||
@@ -525,9 +497,7 @@ | ||
const servicesJson = backends.preparedCsnToEdmAll(oDataCsn, internalOptions); | ||
const servicesJson = preparedCsnToEdmAll(oDataCsn, internalOptions); | ||
const services = servicesJson.edmj; | ||
for (const serviceName in services) { | ||
const lEdm = services[serviceName]; | ||
// FIXME: Why only metadata_json - isn't this rather a 'combined_json' ? If so, rename it! | ||
result[serviceName] = lEdm; | ||
} | ||
for (const serviceName in services) | ||
result[serviceName] = services[serviceName]; | ||
return result; | ||
@@ -554,7 +524,7 @@ } | ||
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata'); | ||
services = backends.preparedCsnToEdmx(csn, service, internalOptions); | ||
services = preparedCsnToEdmx(csn, service, internalOptions); | ||
} | ||
else { | ||
const oDataCsn = odataInternal(csn, internalOptions); | ||
services = backends.preparedCsnToEdmx(oDataCsn, service, internalOptions); | ||
services = preparedCsnToEdmx(oDataCsn, service, internalOptions); | ||
} | ||
@@ -586,3 +556,3 @@ | ||
const servicesEdmx = backends.preparedCsnToEdmxAll(oDataCsn, internalOptions); | ||
const servicesEdmx = preparedCsnToEdmxAll(oDataCsn, internalOptions); | ||
const services = servicesEdmx.edmx; | ||
@@ -599,2 +569,75 @@ // Create annotations and metadata once per service | ||
/** | ||
* Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
* using 'options' | ||
* | ||
* @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. | ||
* @param {string} service Service name to use. If you want all services, use preparedCsnToEdmxAll() | ||
* @param {ODataOptions} options OData / EDMX specific options. | ||
* @returns {object} Rendered EDMX string for the given service. | ||
*/ | ||
function preparedCsnToEdmx(csn, service, options) { | ||
const e = csn2edm(csn, service, options); | ||
return { | ||
edmx: (e ? e.toXML('all') : undefined), | ||
}; | ||
} | ||
/** | ||
* Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
* using 'options'. | ||
* | ||
* @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. | ||
* @param {ODataOptions} options OData / EDMX specific options. | ||
* @returns {object} Dictionary of rendered EDMX strings for each service. | ||
*/ | ||
function preparedCsnToEdmxAll(csn, options) { | ||
const edmxResult = csn2edmAll(csn, options); | ||
for (const service in edmxResult) | ||
edmxResult[service] = edmxResult[service].toXML('all'); | ||
return { | ||
edmx: edmxResult, | ||
}; | ||
} | ||
/** | ||
* Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
* using 'options' | ||
* | ||
* @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. | ||
* @param {string} service Service name for which EDMX should be rendered. | ||
* @param {ODataOptions} options OData / EDMX specific options. | ||
* @returns {object} Rendered EDM JSON object for of the given service. | ||
*/ | ||
function preparedCsnToEdm(csn, service, options) { | ||
// Override OData version as edm json is always v4 | ||
options.odataVersion = 'v4'; | ||
const e = csn2edm(csn, service, options); | ||
return { | ||
edmj: (e ? e.toJSON() : undefined), | ||
}; | ||
} | ||
/** | ||
* Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
* using 'options' | ||
* | ||
* @param {CSN.Model} csn Input CSN model. Must be OData transformed CSN. | ||
* @param {ODataOptions} options OData / EDMX specific options. | ||
* @returns {object} Dictionary of rendered EDM JSON objects for each service. | ||
*/ | ||
function preparedCsnToEdmAll(csn, options) { | ||
// Override OData version as edm json is always v4 | ||
options.odataVersion = 'v4'; | ||
const edmj = csn2edmAll(csn, options); | ||
for (const service in edmj) | ||
edmj[service] = edmj[service].toJSON(); | ||
return { | ||
edmj, | ||
}; | ||
} | ||
/** | ||
* Flatten the result structure to a flat map. | ||
@@ -632,3 +675,5 @@ * | ||
for_hdbcds: publishCsnProcessor(forHdbcds, 'for.hdbcds'), | ||
/** */ | ||
/** Deprecated, will be removed in cds-compiler@v4 */ | ||
preparedCsnToEdmx, | ||
preparedCsnToEdm, | ||
}; | ||
@@ -666,6 +711,11 @@ | ||
try { | ||
if (options.deprecated) { | ||
const messageFunctions = messages.makeMessageFunction(csn, options, 'api'); | ||
checkRemovedDeprecatedFlags( options, messageFunctions ); | ||
} | ||
checkOutdatedOptions( options ); | ||
return processor( csn, options, ...args ); | ||
} | ||
catch (err) { | ||
if (err instanceof CompilationError || options.noRecompile || isPreTransformed(csn, 'odata')) // we cannot recompile a pre-transformed CSN | ||
if (err instanceof messages.CompilationError || options.noRecompile || isPreTransformed(csn, 'odata')) // we cannot recompile a pre-transformed CSN | ||
throw err; | ||
@@ -677,4 +727,4 @@ | ||
const { info } = makeMessageFunction( csn, options, 'compile' ); | ||
const msg = info( 'api-recompiled-csn', emptyLocation('csn.json'), {}, 'CSN input had to be recompiled' ); | ||
const { info } = messages.makeMessageFunction( csn, options, 'compile' ); | ||
const msg = info( 'api-recompiled-csn', location.emptyLocation('csn.json'), {}, 'CSN input had to be recompiled' ); | ||
if (options.internalMsg) | ||
@@ -684,4 +734,4 @@ msg.error = err; // Attach original error | ||
// next line to be replaced by CSN parser call which reads the CSN object | ||
const xsn = recompileX(csn, options); | ||
const recompiledCsn = compactModel(xsn); | ||
const xsn = compiler.recompileX(csn, options); | ||
const recompiledCsn = toCsn.compactModel(xsn); | ||
return processor( recompiledCsn, options, ...args ); | ||
@@ -692,4 +742,71 @@ } | ||
// Note: No toCsn, because @sap/cds may still use it (2022-06-15) | ||
const oldBackendOptionNames = [ 'toSql', 'toOdata', 'toHana', 'forHana' ]; | ||
/** | ||
* Checks if outdated options are used and if so, throw a compiler error. | ||
* These include: | ||
* - magicVars (now variableReplacements) | ||
* - toOdata/toSql/toHana/forHana -> now flat options | ||
* | ||
* @param {CSN.Options} options Backend options | ||
*/ | ||
function checkOutdatedOptions(options) { | ||
const { error, throwWithError } = messages.makeMessageFunction(null, options, 'api'); | ||
// This error has been emitted once, we don't need to emit it again. | ||
if (options.messages?.some(m => m.messageId === 'api-invalid-option')) { | ||
throwWithError(); | ||
return; | ||
} | ||
for (const name of oldBackendOptionNames) { | ||
if (typeof options[name] === 'object') // may be a boolean due to internal options | ||
error('api-invalid-option', null, { '#': 'std', name }); | ||
} | ||
if (options.magicVars) | ||
error('api-invalid-option', null, { '#': 'magicVars' }); | ||
// Don't check `options.magicVars`. It's likely that the user renamed `magicVars` but | ||
// forgot about user -> $user and locale -> $user.locale | ||
if (options.variableReplacements?.user) | ||
error('api-invalid-option', null, { '#': 'user' }); | ||
if (options.variableReplacements?.locale) | ||
error('api-invalid-option', null, { '#': 'locale' }); | ||
forEachKey(options.variableReplacements || {}, (name) => { | ||
if (!name.startsWith('$') && name !== 'user' && name !== 'locale') | ||
error('api-invalid-option', null, { '#': 'noDollar', name }); | ||
}); | ||
throwWithError(); | ||
} | ||
/** | ||
* Load the module on-demand and not immediately. | ||
* | ||
* @param {string} moduleName Name of the module to load - like with require | ||
* @returns {object} A Proxy that handles the on-demand loading | ||
*/ | ||
function lazyload(moduleName) { | ||
let module; | ||
return new Proxy(((...args) => { | ||
if (!module) // eslint-disable-next-line global-require | ||
module = require(moduleName); | ||
if (module.apply && typeof module.apply === 'function') | ||
return module.apply(this, args); | ||
return module; // for destructured calls | ||
}), { | ||
get(target, name) { | ||
if (!module) // eslint-disable-next-line global-require | ||
module = require(moduleName); | ||
return module[name]; | ||
}, | ||
}); | ||
} | ||
/** | ||
* Option format used by the old API, where they are grouped thematically. | ||
@@ -696,0 +813,0 @@ * |
@@ -27,3 +27,3 @@ 'use strict'; | ||
'joinfk', | ||
'magicVars', // deprecated | ||
'magicVars', // deprecated, not removed in v3 as we have specific error messages for it | ||
'variableReplacements', | ||
@@ -111,7 +111,2 @@ // ODATA | ||
for (const optionName in options) { | ||
const optionValue = options[optionName]; | ||
mapToOldNames(optionName, optionValue); | ||
} | ||
// Convenience for $user -> $user.id replacement | ||
@@ -121,34 +116,2 @@ if (options.variableReplacements && options.variableReplacements.$user && typeof options.variableReplacements.$user === 'string') | ||
/** | ||
* Map a new-style option to it's old format | ||
* | ||
* @param {string} optionName Name of the option to map | ||
* @param {any} optionValue Value of the option to map | ||
*/ | ||
function mapToOldNames(optionName, optionValue) { | ||
// Keep all input options and add the "compatibility" options | ||
switch (optionName) { | ||
case 'beta': | ||
options.betaMode = optionValue; | ||
break; | ||
case 'odataVersion': | ||
options.version = optionValue; | ||
break; | ||
case 'sqlDialect': | ||
options.dialect = optionValue; | ||
break; | ||
case 'sqlMapping': | ||
options.names = optionValue; | ||
break; | ||
// No need to remap variableReplacements here - we use the new mechanism with that directly | ||
case 'magicVars': | ||
if (optionValue.user) | ||
options.user = optionValue.user; | ||
if (optionValue.locale) | ||
options.locale = optionValue.locale; | ||
break; | ||
default: break; | ||
} | ||
} | ||
return options; | ||
@@ -161,74 +124,41 @@ } | ||
sql: (options) => { | ||
const hardOptions = { src: 'sql' }; | ||
const hardOptions = { src: 'sql', toSql: true, forHana: true }; | ||
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'plain' }; | ||
const processed = translateOptions(options, defaultOptions, hardOptions, undefined, [ 'sql-dialect-and-naming' ], 'to.sql'); | ||
const result = Object.assign({}, processed); | ||
result.toSql = Object.assign({}, processed); | ||
return result; | ||
return Object.assign({}, processed); | ||
}, | ||
hdi: (options) => { | ||
const hardOptions = { src: 'hdi' }; | ||
const hardOptions = { src: 'hdi', toSql: true, forHana: true }; | ||
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' }; | ||
const processed = translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdi'); | ||
const result = Object.assign({}, processed); | ||
result.toSql = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdi'); | ||
}, | ||
hdbcds: (options) => { | ||
const hardOptions = { forHana: true }; | ||
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' }; | ||
const processed = translateOptions(options, defaultOptions, {}, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdbcds'); | ||
const result = Object.assign({}, processed); | ||
result.forHana = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) }, undefined, 'to.hdbcds'); | ||
}, | ||
edm: (options) => { | ||
const hardOptions = { json: true, combined: true }; | ||
const hardOptions = { json: true, combined: true, toOdata: true }; | ||
const defaultOptions = { odataVersion: 'v4', odataFormat: 'flat' }; | ||
const processed = translateOptions(options, defaultOptions, hardOptions, { odataVersion: generateStringValidator([ 'v4' ]) }, [ 'valid-structured' ], 'to.edm'); | ||
const result = Object.assign({}, processed); | ||
result.toOdata = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, { odataVersion: generateStringValidator([ 'v4' ]) }, [ 'valid-structured' ], 'to.edm'); | ||
}, | ||
edmx: (options) => { | ||
const hardOptions = { xml: true, combined: true }; | ||
const hardOptions = { xml: true, combined: true, toOdata: true }; | ||
const defaultOptions = { | ||
odataVersion: 'v4', odataFormat: 'flat', | ||
}; | ||
const processed = translateOptions(options, defaultOptions, hardOptions, undefined, [ 'valid-structured' ], 'to.edmx'); | ||
const result = Object.assign({}, processed); | ||
result.toOdata = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, undefined, [ 'valid-structured' ], 'to.edmx'); | ||
}, | ||
}, | ||
for: { // TODO: Rename version to oDataVersion | ||
odata: (options) => { | ||
const hardOptions = { toOdata: true }; | ||
const defaultOptions = { odataVersion: 'v4', odataFormat: 'flat' }; | ||
const processed = translateOptions(options, defaultOptions, undefined, undefined, [ 'valid-structured' ], 'for.odata'); | ||
const result = Object.assign({}, processed); | ||
result.toOdata = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, undefined, [ 'valid-structured' ], 'for.odata'); | ||
}, | ||
hana: (options) => { | ||
const hardOptions = { forHana: true }; | ||
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' }; | ||
const processed = translateOptions(options, defaultOptions, undefined, undefined, undefined, 'for.hana'); | ||
const result = Object.assign({}, processed); | ||
result.forHana = Object.assign({}, processed); | ||
return result; | ||
return translateOptions(options, defaultOptions, hardOptions, undefined, undefined, 'for.hana'); | ||
}, | ||
@@ -235,0 +165,0 @@ }, |
@@ -68,9 +68,2 @@ 'use strict'; | ||
}, | ||
magicVars: { | ||
validate: val => val !== null && typeof val === 'object' && !Array.isArray(val), | ||
expected: () => 'type object', | ||
found: (val) => { | ||
return val === null ? val : `type ${ typeof val }`; | ||
}, | ||
}, | ||
// TODO: Maybe do a deep validation of the whole object with leafs? | ||
@@ -89,3 +82,3 @@ variableReplacements: { | ||
}, | ||
sqlDialect: generateStringValidator([ 'sqlite', 'hana', 'plain' ]), | ||
sqlDialect: generateStringValidator([ 'sqlite', 'hana', 'plain', 'postgres' ]), | ||
sqlMapping: generateStringValidator([ 'plain', 'quoted', 'hdbcds' ]), | ||
@@ -136,2 +129,3 @@ odataVersion: generateStringValidator([ 'v2', 'v4' ]), | ||
severity: 'error', | ||
getParameters: () => {}, | ||
getMessage: () => 'Structured OData is only supported with OData version v4', | ||
@@ -142,3 +136,4 @@ }, | ||
severity: 'error', | ||
getMessage: options => `sqlDialect '${ options.sqlDialect }' can't be combined with sqlMapping '${ options.sqlMapping }'`, | ||
getParameters: options => ({ name: options.sqlDialect, prop: options.sqlMapping }), | ||
getMessage: () => 'sqlDialect $(NAME) can\'t be combined with sqlMapping $(PROP)', | ||
}, | ||
@@ -148,2 +143,3 @@ 'beta-no-test': { | ||
severity: 'warning', | ||
getParameters: () => {}, | ||
getMessage: () => 'Option "beta" was used. This option should not be used in productive scenarios!', | ||
@@ -184,3 +180,3 @@ }, | ||
if (combinationValidator.validate(options)) | ||
message[combinationValidator.severity]('invalid-option-combination', null, {}, combinationValidator.getMessage(options)); | ||
message[combinationValidator.severity]('invalid-option-combination', null, combinationValidator.getParameters(options), combinationValidator.getMessage(options)); | ||
} | ||
@@ -187,0 +183,0 @@ |
'use strict'; | ||
const { functionsWithoutParens } = require("../compiler/builtins"); | ||
const { functionsWithoutParens } = require('../compiler/builtins'); | ||
@@ -29,5 +29,5 @@ module.exports = { | ||
'SOME', | ||
'WHEN', | ||
'TRIM', | ||
'TRUE', // boolean | ||
'WHEN', | ||
'WHERE', | ||
@@ -49,2 +49,3 @@ 'WITH', | ||
'ALTER', | ||
'ALWAYS', | ||
'ANALYZE', | ||
@@ -90,2 +91,3 @@ 'AND', | ||
'EXCEPT', | ||
'EXCLUDE', | ||
'EXCLUSIVE', | ||
@@ -96,2 +98,3 @@ 'EXISTS', | ||
'FILTER', | ||
'FIRST', | ||
'FOLLOWING', | ||
@@ -102,4 +105,6 @@ 'FOR', | ||
'FULL', | ||
'GENERATED', | ||
'GLOB', | ||
'GROUP', | ||
'GROUPS', | ||
'HAVING', | ||
@@ -122,2 +127,3 @@ 'IF', | ||
'KEY', | ||
'LAST', | ||
'LEFT', | ||
@@ -127,2 +133,3 @@ 'LIKE', | ||
'MATCH', | ||
'MATERIALIZED', | ||
'NATURAL', | ||
@@ -134,2 +141,3 @@ 'NO', | ||
'NULL', | ||
'NULLS', | ||
'OF', | ||
@@ -140,2 +148,3 @@ 'OFFSET', | ||
'ORDER', | ||
'OTHERS', | ||
'OUTER', | ||
@@ -159,2 +168,3 @@ 'OVER', | ||
'RESTRICT', | ||
'RETURNING', | ||
'RIGHT', | ||
@@ -171,2 +181,3 @@ 'ROLLBACK', | ||
'THEN', | ||
'TIES', | ||
'TO', | ||
@@ -188,12 +199,3 @@ 'TRANSACTION', | ||
'WITH', | ||
'WITHOUT', | ||
'ALWAYS', | ||
'EXCLUDE', | ||
'FIRST', | ||
'GENERATED', | ||
'GROUPS', | ||
'LAST', | ||
'NULLS', | ||
'OTHERS', | ||
'TIES', | ||
'WITHOUT' | ||
], | ||
@@ -206,86 +208,4 @@ // SAP HANA keywords, used for smart quoting in to-hdi.plain | ||
hana: [ | ||
'ALL', | ||
'ALTER', | ||
'AS', | ||
'BEFORE', | ||
'BEGIN', | ||
'BOTH', | ||
'CASE', | ||
'CHAR', | ||
'CONDITION', | ||
'CONNECT', | ||
'CROSS', | ||
'CUBE', | ||
'CURRENT_CONNECTION', | ||
'CURRENT_DATE', | ||
'CURRENT_SCHEMA', | ||
'CURRENT_TIME', | ||
'CURRENT_TIMESTAMP', | ||
'CURRENT_TRANSACTION_ISOLATION_LEVEL', | ||
'CURRENT_USER', | ||
'CURRENT_UTCDATE', | ||
'CURRENT_UTCTIME', | ||
'CURRENT_UTCTIMESTAMP', | ||
'CURRVAL', | ||
'CURSOR', | ||
'DECLARE', | ||
'DISTINCT', | ||
'ELSE', | ||
'ELSEIF', | ||
'END', | ||
'EXCEPT', | ||
'EXCEPTION', | ||
'EXEC', | ||
'FALSE', | ||
'FOR', | ||
'FROM', | ||
'FULL', | ||
'GROUP', | ||
'HAVING', | ||
'IF', | ||
'IN', | ||
'INNER', | ||
'INOUT', | ||
'INTERSECT', | ||
'INTO', | ||
'IS', | ||
'JOIN', | ||
'LEADING', | ||
'LEFT', | ||
'LIMIT', | ||
'LOOP', | ||
'MINUS', | ||
'NATURAL', | ||
'NCHAR', | ||
'NEXTVAL', | ||
'NULL', | ||
'ON', | ||
'ORDER', | ||
'OUT', | ||
'PRIOR', | ||
'RETURN', | ||
'RETURNS', | ||
'REVERSE', | ||
'RIGHT', | ||
'ROLLUP', | ||
'ROWID', | ||
'SELECT', | ||
'SESSION_USER', | ||
'SET', | ||
'SQL', | ||
'START', | ||
'SYSUUID', | ||
'TABLESAMPLE', | ||
'TOP', | ||
'TRAILING', | ||
'TRUE', | ||
'UNION', | ||
'UNKNOWN', | ||
'USING', | ||
'UTCTIMESTAMP', | ||
'VALUES', | ||
'WHEN', | ||
'WHERE', | ||
'WHILE', | ||
'WITH', | ||
'ABAPITAB', | ||
'ABAPSTRUCT', | ||
'ABAP_CHAR', | ||
@@ -305,4 +225,2 @@ 'ABAP_DATE', | ||
'ABAP_XSTRING', | ||
'ABAPITAB', | ||
'ABAPSTRUCT', | ||
'ADD_DAYS', | ||
@@ -313,2 +231,4 @@ 'ADD_MONTHS', | ||
'ADOPT', | ||
'ALL', | ||
'ALTER', | ||
'ANALYTIC', | ||
@@ -319,2 +239,3 @@ 'ANY', | ||
'ARRAY_AGG', | ||
'AS', | ||
'AT', | ||
@@ -325,2 +246,4 @@ 'AUTHORIZATION', | ||
'BASIC', | ||
'BEFORE', | ||
'BEGIN', | ||
'BETWEEN', | ||
@@ -339,2 +262,3 @@ 'BIGINT', | ||
'BOOLEAN', | ||
'BOTH', | ||
'BOUNDARY', | ||
@@ -344,2 +268,3 @@ 'BREAKUP', | ||
'BY', | ||
'CASE', | ||
'CAST', | ||
@@ -349,2 +274,3 @@ 'CE_CALC', | ||
'CE_PROJECTION', | ||
'CHAR', | ||
'CHARACTER', | ||
@@ -355,5 +281,8 @@ 'CLOB', | ||
'CONCAT', | ||
'CONDITION', | ||
'CONNECT', | ||
'CONSTANT', | ||
'CONSTRAINT', | ||
'COUNT', | ||
'CROSS', | ||
'CS_DATE', | ||
@@ -380,5 +309,18 @@ 'CS_DAYDATE', | ||
'CS_TIME', | ||
'CUBE', | ||
'CUME_DIST', | ||
'CURDATE', | ||
'CURRENT_CONNECTION', | ||
'CURRENT_DATABASE', | ||
'CURRENT_DATE', | ||
'CURRENT_SCHEMA', | ||
'CURRENT_TIME', | ||
'CURRENT_TIMESTAMP', | ||
'CURRENT_TRANSACTION_ISOLATION_LEVEL', | ||
'CURRENT_USER', | ||
'CURRENT_UTCDATE', | ||
'CURRENT_UTCTIME', | ||
'CURRENT_UTCTIMESTAMP', | ||
'CURRVAL', | ||
'CURSOR', | ||
'CURTIME', | ||
@@ -441,10 +383,19 @@ 'CURVE', | ||
'DECIMAL', | ||
'DECLARE', | ||
'DENSE_RANK', | ||
'DEPTH', | ||
'DISTANCE', | ||
'DISTINCT', | ||
'DOUBLE', | ||
'DW_OPTIMIZED', | ||
'ELSE', | ||
'ELSEIF', | ||
'EMPTY', | ||
'END', | ||
'EXCEPT', | ||
'EXCEPTION', | ||
'EXEC', | ||
'EXISTS', | ||
'EXTRACT', | ||
'FALSE', | ||
'FILTER', | ||
@@ -454,6 +405,10 @@ 'FIRST_VALUE', | ||
'FLOAT', | ||
'FOR', | ||
'FORCE_FIRST_PASSWORD_CHANGE', | ||
'FREESTYLESEARCHATTRIBUTE', | ||
'FROM', | ||
'FULL', | ||
'GET_NUM_SERVERS', | ||
'GREATEST', | ||
'GROUP', | ||
'GROUPING', | ||
@@ -464,2 +419,3 @@ 'GROUPING_FILTER', | ||
'HASSYSTEMPRIVILEGE', | ||
'HAVING', | ||
'HEXTOBIN', | ||
@@ -478,3 +434,7 @@ 'HIERARCHY', | ||
'HOUR', | ||
'IF', | ||
'IFNULL', | ||
'IN', | ||
'INNER', | ||
'INOUT', | ||
'INSTR', | ||
@@ -484,5 +444,9 @@ 'INT', | ||
'INTERNAL', | ||
'IS_EMPTY', | ||
'INTERSECT', | ||
'INTO', | ||
'IS', | ||
'ISAUTHORIZED', | ||
'ISTOTAL', | ||
'IS_EMPTY', | ||
'JOIN', | ||
'JSON_QUERY', | ||
@@ -497,10 +461,14 @@ 'JSON_TABLE', | ||
'LEAD', | ||
'LEADING', | ||
'LEAST', | ||
'LEAVES', | ||
'LEFT', | ||
'LENGTH', | ||
'LENGTHB', | ||
'LEVELS', | ||
'LIMIT', | ||
'LOCATE', | ||
'LOCATE_REGEXPR', | ||
'LONGDATE', | ||
'LOOP', | ||
'LPAD', | ||
@@ -514,6 +482,10 @@ 'LTRIM', | ||
'MIN', | ||
'MINUS', | ||
'MINUTE', | ||
'MONTH', | ||
'MULTIPARENT', | ||
'NATURAL', | ||
'NCHAR', | ||
'NCLOB', | ||
'NEXTVAL', | ||
'NEXT_DAY', | ||
@@ -526,2 +498,3 @@ 'NO', | ||
'NTILE', | ||
'NULL', | ||
'NULLIF', | ||
@@ -535,10 +508,14 @@ 'NUMBER', | ||
'OLYMP', | ||
'ON', | ||
'OPENCYPHER_TABLE', | ||
'ORDER', | ||
'ORDINALITY', | ||
'ORPHAN', | ||
'OUT', | ||
'OVER', | ||
'PERCENT_RANK', | ||
'PERCENTILE_CONT', | ||
'PERCENTILE_DISC', | ||
'PERCENT_RANK', | ||
'PLAIN', | ||
'PRIOR', | ||
'PRODUCT', | ||
@@ -557,5 +534,11 @@ 'RANGE', | ||
'REPLACE_REGEXPR', | ||
'RETURN', | ||
'RETURNS', | ||
'REVERSE', | ||
'RIGHT', | ||
'ROLLUP', | ||
'ROUND', | ||
'ROUNDROBIN', | ||
'ROW', | ||
'ROWID', | ||
'ROW_NUMBER', | ||
@@ -570,2 +553,3 @@ 'RPAD', | ||
'SECONDTIME', | ||
'SELECT', | ||
'SERIES_ELEMENT_TO_PERIOD', | ||
@@ -575,2 +559,4 @@ 'SERIES_PERIOD_TO_ELEMENT', | ||
'SESSION_CONTEXT', | ||
'SESSION_USER', | ||
'SET', | ||
'SIBLING', | ||
@@ -580,2 +566,7 @@ 'SMALLDECIMAL', | ||
'SOME', | ||
'SQL', | ||
'START', | ||
'STDDEV', | ||
'STRING', | ||
'STRING_AGG', | ||
'ST_ALPHASHAPEAGGR', | ||
@@ -621,15 +612,14 @@ 'ST_ALPHASHAPEAREAAGGR', | ||
'ST_UNIONAGGR', | ||
'STDDEV', | ||
'STRING', | ||
'STRING_AGG', | ||
'SUBSTR', | ||
'SUBSTRING', | ||
'SUBSTRING_REGEXPR', | ||
'SUBSTR_AFTER', | ||
'SUBSTR_BEFORE', | ||
'SUBSTR_REGEXPR', | ||
'SUBSTRING', | ||
'SUBSTRING_REGEXPR', | ||
'SUM', | ||
'SYSTEM_TIME', | ||
'SYSUUID', | ||
'TABLE', | ||
'TABLES', | ||
'TABLESAMPLE', | ||
'TARGET', | ||
@@ -652,2 +642,7 @@ 'TEMPORARY', | ||
'TM_GET_SUGGESTED_TERMS', | ||
'TOKEN', | ||
'TOOLOPTION', | ||
'TOP', | ||
'TOPK', | ||
'TOTAL', | ||
'TO_BIGINT', | ||
@@ -678,9 +673,6 @@ 'TO_BINARY', | ||
'TO_VARCHAR', | ||
'TOKEN', | ||
'TOOLOPTION', | ||
'TOPK', | ||
'TOTAL', | ||
'TRACE', | ||
'TRACEPROFILE', | ||
'TRACES', | ||
'TRAILING', | ||
'TRANSACTION', | ||
@@ -690,4 +682,10 @@ 'TREE', | ||
'TRIM', | ||
'TRUE', | ||
'UNION', | ||
'UNKNOWN', | ||
'UNNEST', | ||
'USER', | ||
'USING', | ||
'UTCTIMESTAMP', | ||
'VALUES', | ||
'VAR', | ||
@@ -701,4 +699,8 @@ 'VARBINARY', | ||
'WEEKDAY', | ||
'WHEN', | ||
'WHERE', | ||
'WHILE', | ||
'WHY_FOUND', | ||
'WINDOW', | ||
'WITH', | ||
'WITHIN', | ||
@@ -737,3 +739,108 @@ 'XMLTABLE', | ||
'WHERE', 'WHILE', 'WITH' | ||
], | ||
// Postgres keywords, used for smart quoting in to-sql.plain.postgres | ||
// Taken from https://www.postgresql.org/docs/current/sql-keywords-appendix.html | ||
// Generated via scripts/keywords/postgres/generateKeywords.js | ||
postgres: [ | ||
'ALL', | ||
'ANALYSE', | ||
'ANALYZE', | ||
'AND', | ||
'ANY', | ||
'ARRAY', | ||
'AS', | ||
'ASC', | ||
'ASYMMETRIC', | ||
'AUTHORIZATION', | ||
'BINARY', | ||
'BOTH', | ||
'CASE', | ||
'CAST', | ||
'CHECK', | ||
'COLLATE', | ||
'COLLATION', | ||
'COLUMN', | ||
'CONCURRENTLY', | ||
'CONSTRAINT', | ||
'CREATE', | ||
'CROSS', | ||
'CURRENT_CATALOG', | ||
'CURRENT_DATE', | ||
'CURRENT_ROLE', | ||
'CURRENT_SCHEMA', | ||
'CURRENT_TIME', | ||
'CURRENT_TIMESTAMP', | ||
'CURRENT_USER', | ||
'DEFAULT', | ||
'DEFERRABLE', | ||
'DESC', | ||
'DISTINCT', | ||
'DO', | ||
'ELSE', | ||
'END', | ||
'EXCEPT', | ||
'FALSE', | ||
'FETCH', | ||
'FOR', | ||
'FOREIGN', | ||
'FREEZE', | ||
'FROM', | ||
'FULL', | ||
'GRANT', | ||
'GROUP', | ||
'HAVING', | ||
'ILIKE', | ||
'IN', | ||
'INITIALLY', | ||
'INNER', | ||
'INTERSECT', | ||
'INTO', | ||
'IS', | ||
'ISNULL', | ||
'JOIN', | ||
'LATERAL', | ||
'LEADING', | ||
'LEFT', | ||
'LIKE', | ||
'LIMIT', | ||
'LOCALTIME', | ||
'LOCALTIMESTAMP', | ||
'NATURAL', | ||
'NOT', | ||
'NOTNULL', | ||
'NULL', | ||
'OFFSET', | ||
'ON', | ||
'ONLY', | ||
'OR', | ||
'ORDER', | ||
'OUTER', | ||
'OVERLAPS', | ||
'PLACING', | ||
'PRIMARY', | ||
'REFERENCES', | ||
'RETURNING', | ||
'RIGHT', | ||
'SELECT', | ||
'SESSION_USER', | ||
'SIMILAR', | ||
'SOME', | ||
'SYMMETRIC', | ||
'TABLE', | ||
'TABLESAMPLE', | ||
'THEN', | ||
'TO', | ||
'TRAILING', | ||
'TRUE', | ||
'UNION', | ||
'UNIQUE', | ||
'USER', | ||
'USING', | ||
'VARIADIC', | ||
'VERBOSE', | ||
'WHEN', | ||
'WHERE', | ||
'WINDOW', | ||
'WITH' | ||
] | ||
} |
@@ -48,2 +48,3 @@ // Central registry for messages. | ||
'anno-duplicate-unrelated-layer': { severity: 'Error', configurableFor: true }, // does not hurt us | ||
'anno-unstable-array': { severity: 'Warning' }, | ||
'anno-invalid-sql-element': { severity: 'Error', configurableFor: true }, // @sql.prepend/append - configurable for "I know what I'm doing" | ||
@@ -71,3 +72,3 @@ 'anno-invalid-sql-struct': { severity: 'Error', configurableFor: true }, // @sql.prepend/append - configurable for "I know what I'm doing" | ||
'check-proper-type': { severity: 'Error', configurableFor: [ 'compile' ] }, | ||
'check-proper-type-of': { severity: 'Info', errorFor: [ 'for.odata', 'to.edmx', 'to.hdbcds', 'to.sql', 'to.rename' ] }, | ||
'check-proper-type-of': { severity: 'Info', errorFor: [ 'for.odata', 'to.edmx', 'to.hdbcds', 'to.sql', 'to.hdi', 'to.rename' ] }, | ||
@@ -85,2 +86,3 @@ 'expr-no-filter': { severity: 'Error', configurableFor: 'deprecated' }, | ||
'type-unexpected-typeof': { severity: 'Error', configurableFor: 'deprecated' }, // TODO: make it non-config | ||
'type-ignoring-argument': { severity: 'Error', configurableFor: true }, | ||
'type-expected-builtin': { severity: 'Error', configurableFor: true }, | ||
@@ -122,2 +124,3 @@ 'expected-actionparam-type': { severity: 'Error' }, | ||
'ref-rejected-on': { severity: 'Error' }, | ||
'ref-expected-element': { severity: 'Error' }, | ||
@@ -135,10 +138,7 @@ 'rewrite-key-not-covered-explicit': { severity: 'Error', configurableFor: 'deprecated' }, | ||
'syntax-anno-after-enum': { severity: 'Error', configurableFor: true }, // does not hurt | ||
'syntax-anno-after-params': { severity: 'Error', configurableFor: true }, // does not hurt | ||
'syntax-anno-after-struct': { severity: 'Error', configurableFor: true }, // does not hurt | ||
'syntax-csn-expected-cardinality': { severity: 'Error' }, // TODO: more than 30 chars | ||
'syntax-csn-expected-length': { severity: 'Error' }, | ||
'syntax-csn-expected-translation': { severity: 'Error' }, // TODO: more than 30 chars | ||
'syntax-csn-required-subproperty': { severity: 'Error' }, // TODO: more than 30 chars | ||
'syntax-csn-unexpected-property': { severity: 'Error', configurableFor: true }, // is the removed | ||
'syntax-expected-cardinality': { severity: 'Error' }, | ||
'syntax-expected-length': { severity: 'Error' }, | ||
'syntax-expected-translation': { severity: 'Error' }, | ||
'syntax-required-subproperty': { severity: 'Error' }, | ||
'syntax-unexpected-property': { severity: 'Error', configurableFor: true }, // is the removed | ||
'syntax-deprecated-ident': { severity: 'Error', configurableFor: true }, | ||
@@ -156,2 +156,4 @@ 'syntax-fragile-alias': { severity: 'Error', configurableFor: true }, | ||
'syntax-expected-integer': { severity: 'Error' }, | ||
'syntax-invalid-masked': { severity: 'Error', configurableFor: true }, | ||
'syntax-unexpected-null': { severity: 'Error', configurableFor: true }, | ||
@@ -202,3 +204,13 @@ 'type-managed-composition': { severity: 'Error', configurableFor: 'deprecated' }, // TODO: non-config | ||
const centralMessageTexts = { | ||
'api-invalid-option': { | ||
std: 'Option $(NAME) is deprecated! Use SNAPI options instead', | ||
magicVars: 'Option “magicVars” is deprecated! Use “variableReplacements” instead. See <https://cap.cloud.sap/docs/guides/databases#configuring-variables> for details', | ||
user: 'Option “variableReplacements” expects “$user” instead of “user”. See <https://cap.cloud.sap/docs/guides/databases#configuring-variables> for details', | ||
locale: 'Option “variableReplacements” expects “$user.locale” instead of “locale”. See <https://cap.cloud.sap/docs/guides/databases#configuring-variables> for details', | ||
'noDollar': 'Option “variableReplacements” does not know $(NAME). Did you forget a leading “$”?' | ||
}, | ||
'anno-duplicate': 'Duplicate assignment with $(ANNO)', | ||
'anno-duplicate-unrelated-layer': 'Duplicate assignment with $(ANNO)', | ||
'anno-unstable-array': 'Unstable order of array items due to repeated assignments for $(ANNO) in unrelated layers', | ||
'anno-mismatched-ellipsis': 'An array with $(CODE) can only be used if there is an assignment below with an array value', | ||
@@ -208,11 +220,21 @@ 'anno-unexpected-ellipsis': 'No base annotation available to apply $(CODE)', | ||
'chained-array-of': '"Array of"/"many" must not be chained with another "array of"/"many" inside a service', | ||
'syntax-csn-expected-object': 'Expected object for property $(PROP)', | ||
'syntax-csn-expected-column': 'Expected object or string \'*\' for property $(PROP)', | ||
'syntax-csn-expected-natnum': 'Expected non-negative number for property $(PROP)', | ||
'syntax-csn-expected-cardinality': 'Expected non-negative number or string \'*\' for property $(PROP)', | ||
'syntax-csn-expected-reference': 'Expected non-empty string or object for property $(PROP)', | ||
'syntax-csn-expected-term': 'Expected non-empty string or object for property $(PROP)', | ||
'syntax-anno-after-struct': 'Avoid annotation assignments after structure definitions', | ||
'syntax-anno-after-enum': 'Avoid annotation assignments after enum definitions', | ||
'syntax-anno-after-params': 'Avoid annotation assignments after parameters', | ||
'name-duplicate-element': { | ||
'std': 'Generated element $(NAME) conflicts with another element', | ||
'flatten-element-gen': 'Generated element $(NAME) conflicts with other generated element', | ||
'flatten-element-exist': 'Flattened name of structured element conflicts with existing element $(NAME)', | ||
'flatten-fkey-gen': 'Duplicate definition of foreign key element $(NAME) for association $(ART)', | ||
'flatten-fkey-exists': 'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element', | ||
}, | ||
'syntax-unexpected-ellipsis': { | ||
std: 'Expected no more than one $(CODE)', | ||
'nested-array': 'Unexpected $(CODE) in nested array' | ||
}, | ||
'syntax-expected-object': 'Expected object for property $(PROP)', | ||
'syntax-expected-column': 'Expected object or string \'*\' for property $(PROP)', | ||
'syntax-expected-natnum': 'Expected non-negative number for property $(PROP)', | ||
'syntax-expected-cardinality': 'Expected non-negative number or string \'*\' for property $(PROP)', | ||
'syntax-expected-reference': 'Expected non-empty string or object for property $(PROP)', | ||
'syntax-expected-term': 'Expected non-empty string or object for property $(PROP)', | ||
'syntax-dollar-ident': { | ||
@@ -224,3 +246,3 @@ std: 'An artifact starting with $(NAME) might shadow a special variable - replace by another name', | ||
}, | ||
'syntax-csn-expected-length': { | ||
'syntax-expected-length': { | ||
std: 'Expected array in $(PROP) to have at least $(N) items', | ||
@@ -257,2 +279,16 @@ one: 'Expected array in $(PROP) to have at least one item', | ||
}, | ||
'syntax-invalid-literal': { | ||
'std': 'Invalid literal', | ||
'uneven-hex': 'A binary literal must have an even number of characters', | ||
'invalid-hex': 'A binary literal must only contain characters 0-9, a-f and A-F', | ||
'time': 'Expected time\'hh:mm:ss\' where hh, mm and the optional ss are numbers', | ||
'date': 'Expected date\'YYYY-MM-DD\' where YYYY, MM and DD are numbers', | ||
'timestamp': 'Expected timestamp\'YYYY-MM-DD hh:mm:ss.u…u\' where YYYY, MM, DD, hh, mm, ss and u are numbers (optional 1-7×u)', | ||
}, | ||
'syntax-unexpected-null': 'Keyword $(KEYWORD) must appear after the enum definition and not before', | ||
'syntax-unexpected-vocabulary': { | ||
std: 'Annotations can\'t be defined inside contexts or services', | ||
service: 'Annotations can\'t be defined inside services', | ||
context: 'Annotations can\'t be defined inside contexts', | ||
}, | ||
'ref-undefined-def': { | ||
@@ -279,2 +315,6 @@ std: 'Artifact $(ART) has not been found', | ||
}, | ||
'ref-expected-element': { | ||
std: 'Expected element reference', | ||
magicVar: 'Only elements of magic variable $(ID) can be selected', | ||
}, | ||
'type-unexpected-typeof': { | ||
@@ -437,3 +477,3 @@ std: 'Unexpected $(KEYWORD) for the type reference here', | ||
* Only has an effect if default severity is 'Error'. | ||
* 'deprecated': severity can only be changed with deprecated.downgradableErrors. | ||
* 'deprecated': severity can only be changed with deprecated._downgradableErrors. | ||
* TODO: Value `true` is temporary. Use an array instead. | ||
@@ -440,0 +480,0 @@ * @property {string[]} [errorFor] Array of module names where the message shall be reclassified to an error. |
@@ -30,3 +30,3 @@ // Functions and classes for syntax messages | ||
* | ||
* @param {CSN.Message[]} messages | ||
* @param {CompileMessage[]} messages | ||
* @returns {boolean} | ||
@@ -43,3 +43,3 @@ */ | ||
* | ||
* @param {CSN.Message[]} messages | ||
* @param {CompileMessage[]} messages | ||
* @param {string} moduleName | ||
@@ -324,11 +324,14 @@ * @returns {boolean} | ||
* @param {CSN.Options} [options] | ||
* @param {string} [moduleName] | ||
* @param {string|null} [moduleName] | ||
*/ | ||
function makeMessageFunction( model, options, moduleName = null ) { | ||
// ensure message consistency during runtime with --test-mode | ||
if (options.testMode) | ||
if (options.testMode) { | ||
// ensure message consistency during runtime with --test-mode | ||
_check$Init( options ); | ||
if (!options.messages) | ||
throw new CompilerAssertion('makeMessageFunction() expects options.messages to exist in testMode!'); | ||
} | ||
const hasMessageArray = !!options.messages; | ||
const deprecatedDowngradable = isDeprecatedEnabled( options, 'downgradableErrors' ); | ||
const deprecatedDowngradable = isDeprecatedEnabled( options, '_downgradableErrors' ); | ||
/** | ||
@@ -338,3 +341,3 @@ * Array of collected compiler messages. Only use it for debugging. Will not | ||
* | ||
* @type {CSN.Message[]} | ||
* @type {CompileMessage[]} | ||
*/ | ||
@@ -368,3 +371,3 @@ let messages = options.messages || []; | ||
/** @type {CSN.Message} */ | ||
/** @type {CompileMessage} */ | ||
const msg = new CompileMessage( fileLocation, text, severity, id, semanticLocation, moduleName ); | ||
@@ -561,3 +564,3 @@ if (options.internalMsg) | ||
* @param {...any} args | ||
* @returns {CSN.Message[]} | ||
* @returns {CompileMessage[]} | ||
*/ | ||
@@ -588,2 +591,18 @@ function callTransparently(callback, ...args) { | ||
/** | ||
* Check the consistency of the given message and run some basic lint checks. These include: | ||
* | ||
* - Long message IDs must be listed centrally. | ||
* - Messages with the same ID must have the same severity (in a module). | ||
* - Messages with the same ID must have the same message texts. | ||
* This ensures that $(PLACEHOLDERS) are used and that we don't accidentally | ||
* use the same ID for different meanings, i.e. texts. | ||
* | ||
* @param {string} id | ||
* @param {string} moduleName | ||
* @param {string} severity | ||
* @param {string|object} texts | ||
* @param {CSN.Options} options | ||
* @private | ||
*/ | ||
function _check$Consistency( id, moduleName, severity, texts, options ) { | ||
@@ -599,2 +618,12 @@ if (id.length > 30 && !centralMessages[id]) | ||
/** | ||
* Check the consistency of the message severity for the given message ID. | ||
* Messages with the same ID must have the same severity (in a module). | ||
* Non-downgradable errors must never be called with a lower severity. | ||
* | ||
* @param {string} id | ||
* @param {string} moduleName | ||
* @param {string} severity | ||
* @private | ||
*/ | ||
function _check$Severities( id, moduleName, severity ) { | ||
@@ -609,3 +638,3 @@ if (!severity) // if just used message(), we are automatically consistent | ||
else if (expected !== severity) | ||
throw new CompilerAssertion( `Expecting severity "${expected}" from previous call, not "${severity}" for message ID "${id}"` ); | ||
throw new CompilerAssertion( `Inconsistent severity: Expecting "${expected}" from previous call, not "${severity}" for message ID "${id}"` ); | ||
return; | ||
@@ -616,12 +645,24 @@ } | ||
if (severity !== 'Error') | ||
throw new CompilerAssertion( `Expecting severity "Error", not "${severity}" for message ID "${id}" in module "${moduleName}"` ); | ||
throw new CompilerAssertion( `Inconsistent severity: Expecting "Error", not "${severity}" for message ID "${id}" in module "${moduleName}"` ); | ||
} | ||
else if (spec.severity === 'Error') { | ||
throw new CompilerAssertion( `Expecting the use of function message() when message ID "${id}" is a configurable error in module "${moduleName}"` ); | ||
throw new CompilerAssertion( `Inconsistent severity: Expecting the use of function message() when message ID "${id}" is a configurable error in module "${moduleName}"` ); | ||
} | ||
else if (spec.severity !== severity) { | ||
throw new CompilerAssertion( `Expecting severity "${spec.severity}", not "${severity}" for message ID "${id}" in module "${moduleName}"` ); | ||
throw new CompilerAssertion( `Inconsistent severity: Expecting "${spec.severity}", not "${severity}" for message ID "${id}" in module "${moduleName}"` ); | ||
} | ||
} | ||
/** | ||
* Check the consistency of the message text for the given message ID. | ||
* | ||
* Messages with the same ID must have the same message texts. | ||
* This ensures that $(PLACEHOLDERS) are used and that we don't accidentally | ||
* use the same ID for different meanings, i.e. texts. | ||
* | ||
* @param {string} id | ||
* @param {string} prop | ||
* @param {string} value | ||
* @private | ||
*/ | ||
function _check$Texts( id, prop, value ) { | ||
@@ -634,3 +675,3 @@ if (!test$texts[id]) | ||
else if (expected !== value) | ||
throw new CompilerAssertion( `Expecting text "${expected}", not "${value}" for message ID "${id}" and text variant "${prop}"`); | ||
throw new CompilerAssertion( `Different texts for the same message ID. Expecting "${expected}", not "${value}" for ID "${id}" and text variant "${prop}"`); | ||
} | ||
@@ -833,3 +874,3 @@ | ||
* | ||
* @param {CSN.Message} err | ||
* @param {CompileMessage} err | ||
* @param {boolean} [normalizeFilename] | ||
@@ -856,3 +897,3 @@ * @param {boolean} [noMessageId] | ||
* | ||
* @param {CSN.Message} msg | ||
* @param {CompileMessage} msg | ||
* @returns {string} can be used to uniquely identify a message | ||
@@ -883,3 +924,3 @@ */ | ||
* | ||
* @param {CSN.Message} err | ||
* @param {CompileMessage} err | ||
* @param {object} [config = {}] | ||
@@ -932,3 +973,3 @@ * @param {boolean} [config.normalizeFilename] Replace windows `\` with forward slashes `/`. | ||
* from `lib/utils/file.js` | ||
* @param {CSN.Message} err Error object containing all details like line, message, etc. | ||
* @param {CompileMessage} err Error object containing all details like line, message, etc. | ||
* @param {object} [config = {}] | ||
@@ -1012,4 +1053,4 @@ * @param {boolean | 'auto'} [config.color] If true, ANSI escape codes will be used for coloring the `^`. If false, no | ||
* | ||
* @param {CSN.Message} a | ||
* @param {CSN.Message} b | ||
* @param {CompileMessage} a | ||
* @param {CompileMessage} b | ||
*/ | ||
@@ -1048,4 +1089,4 @@ function compareMessage( a, b ) { | ||
* | ||
* @param {CSN.Message} a | ||
* @param {CSN.Message} b | ||
* @param {CompileMessage} a | ||
* @param {CompileMessage} b | ||
*/ | ||
@@ -1062,3 +1103,3 @@ function compareMessageSeverityAware( a, b ) { | ||
* | ||
* @param {CSN.Message} msg | ||
* @param {CompileMessage} msg | ||
*/ | ||
@@ -1082,3 +1123,3 @@ function homeSortName( { home, messageId } ) { | ||
* | ||
* @param {CSN.Message[]} messages | ||
* @param {CompileMessage[]} messages | ||
*/ | ||
@@ -1085,0 +1126,0 @@ function deduplicateMessages( messages ) { |
'use strict'; | ||
const { forEach } = require("../utils/objectUtils"); | ||
const queryOps = { | ||
@@ -23,3 +25,2 @@ query: 'select', // TODO: rename to SELECT | ||
toRename: true, | ||
addTextsLanguageAssoc: true, | ||
assocsWithParams: true, | ||
@@ -62,3 +63,3 @@ hanaAssocRealCardinality: true, | ||
* @param {object} options Options | ||
* @param {string} [feature] Feature to check for | ||
* @param {string|null} [feature] Feature to check for | ||
* @returns {boolean} | ||
@@ -70,5 +71,45 @@ */ | ||
return !!deprecated; | ||
return deprecated && typeof deprecated === 'object' && deprecated[feature]; | ||
} | ||
const oldDeprecatedFlags_v2 = [ | ||
'createLocalizedViews', | ||
'downgradableErrors', | ||
'generatedEntityNameWithUnderscore', | ||
'longAutoexposed', | ||
'noElementsExpansion', | ||
'noInheritedAutoexposeViaComposition', | ||
'noScopedRedirections', | ||
'oldVirtualNotNullPropagation', | ||
'parensAsStrings', | ||
'projectionAsQuery', | ||
'redirectInSubQueries', | ||
'renderVirtualElements', | ||
'shortAutoexposed', | ||
'unmanagedUpInComponent', | ||
'v1KeysForTemporal', | ||
]; | ||
/** | ||
* In cds-compiler v3, we removed old v2 deprecated flags. That can lead to silent | ||
* errors such as entity/view names changing. To ensure that the user is forced | ||
* to change their code, emit an error if one of such removed flags was used. | ||
* | ||
* @param {CSN.Options} options | ||
* @param error Error message function returned by makeMessageFunctions(). | ||
*/ | ||
function checkRemovedDeprecatedFlags( options, { error } ) { | ||
// Assume that we emitted these errors once if a message with this ID was found. | ||
if (!options.deprecated || options.messages?.some(m => m.messageId === 'api-invalid-deprecated')) | ||
return; | ||
forEach(options.deprecated, (key, val) => { | ||
if (val && oldDeprecatedFlags_v2.includes(key)) { | ||
error('api-invalid-deprecated', null, { name: key }, | ||
'Deprecated flag $(NAME) has been removed in CDS compiler v3'); | ||
} | ||
}); | ||
} | ||
// Apply function `callback` to all artifacts in dictionary | ||
@@ -135,2 +176,3 @@ // `model.definitions`. See function `forEachGeneric` for details. | ||
isDeprecatedEnabled, | ||
checkRemovedDeprecatedFlags, | ||
queryOps, | ||
@@ -137,0 +179,0 @@ forEachDefinition, |
@@ -19,4 +19,4 @@ 'use strict'; | ||
// const isMultiSchema = this.options.toOdata.odataFormat === 'structured' && | ||
// (this.options.toOdata.odataProxies || this.options.toOdata.odataXServiceRefs); | ||
// const isMultiSchema = this.options.odataFormat === 'structured' && | ||
// (this.options.odataProxies || this.options.odataXServiceRefs); | ||
@@ -89,5 +89,7 @@ const serviceName = this.csnUtils.getServiceName(artName); | ||
function checkReturns(returns, currPath, actKind) { | ||
const finalReturnType = returns.type ? this.csnUtils.getFinalBaseType(returns.type) : returns; | ||
const finalReturnType = returns.type ? this.csnUtils.getFinalBaseTypeWithProps(returns.type) : returns; | ||
if (!finalReturnType) | ||
return; // no type, e.g. `type of V:calculated`; already an error in `checkTypeOfHasProperType()` | ||
if (this.csnUtils.isAssocOrComposition(finalReturnType)) { | ||
if (this.csnUtils.isAssocOrComposition(finalReturnType.type)) { | ||
this.error(null, currPath, { '#': actKind }, | ||
@@ -103,3 +105,3 @@ { | ||
checkReturns.bind(this)(finalReturnType.items, currPath.concat('items'), actKind); | ||
else // check if return type is user definited from the current service | ||
else // check if return type is user defined from the current service | ||
checkUserDefinedType.bind(this)(finalReturnType, returns.type, currPath); | ||
@@ -106,0 +108,0 @@ } |
@@ -26,3 +26,3 @@ 'use strict'; | ||
}; | ||
if (member['@Core.MediaType'] && member.type && !(this.csnUtils.getFinalBaseType(member.type) in allowedCoreMediaTypes)) { | ||
if (member['@Core.MediaType'] && member.type && !(this.csnUtils.getFinalBaseTypeWithProps(member.type)?.type in allowedCoreMediaTypes)) { | ||
this.warning(null, member.$path, { names: [ 'Edm.String', 'Edm.Binary' ] }, | ||
@@ -29,0 +29,0 @@ 'Element annotated with “@Core.MediaType” should be of a type mapped to $(NAMES)'); |
@@ -17,2 +17,3 @@ 'use strict'; | ||
const persistenceAnnos = [ '@cds.persistence.table', '@cds.persistence.udf', '@cds.persistence.calcview' ]; | ||
// TODO: Why not filter over persistenceAnnos, is shorter! | ||
const TableUdfCv = Object.keys(artifact).filter(p => persistenceAnnos.includes(p) && artifact[p]); | ||
@@ -19,0 +20,0 @@ if (TableUdfCv.length > 1) |
@@ -39,6 +39,6 @@ 'use strict'; | ||
if (member.key || parentIsKey) { | ||
const finalBaseType = this.csnUtils.getFinalBaseType(member.type); | ||
if (typeof finalBaseType === 'string' && isGeoTypeName(finalBaseType)) { | ||
const finalBaseType = this.csnUtils.getFinalBaseTypeWithProps(member.type); | ||
if (isGeoTypeName(finalBaseType?.type)) { | ||
this.error(null, parentPath || member.$path, | ||
{ type: finalBaseType, name: elemFqName }, | ||
{ type: finalBaseType.type, name: elemFqName }, | ||
'Type $(TYPE) can\'t be used as primary key in element $(NAME)'); | ||
@@ -67,3 +67,3 @@ } | ||
if (member.key || parentIsKey) { | ||
const finalBaseType = this.csnUtils.getFinalBaseType(member.type); | ||
const finalBaseType = this.csnUtils.getFinalBaseTypeWithProps(member.type); | ||
if (member.items || (finalBaseType && finalBaseType.items)) { | ||
@@ -101,6 +101,6 @@ this.error(null, parentPath || member.$path, { name: elemFqName }, | ||
* Checks whether managed associations | ||
* with cardinality 'to many' have an on-condition | ||
* and if managed associations have foreign keys. | ||
* with cardinality 'to many' have an on-condition. | ||
* | ||
* @param {CSN.Artifact} art The artifact | ||
* @todo this is a member validator, is it not? | ||
*/ | ||
@@ -107,0 +107,0 @@ function checkManagedAssoc(art) { |
@@ -29,3 +29,3 @@ 'use strict'; | ||
if (target.kind !== 'entity') { | ||
const isAssoc = this.csnUtils.getFinalBaseType(member.type) !== 'cds.Composition'; | ||
const isAssoc = this.csnUtils.getFinalBaseTypeWithProps(member.type)?.type !== 'cds.Composition'; | ||
this.error( | ||
@@ -32,0 +32,0 @@ null, |
@@ -24,3 +24,3 @@ 'use strict'; | ||
// Paths of an expression may end on a structured element only if both operands in the expression end on a structured element | ||
if (_art.elements && !validStructuredElement && $scope !== '$self') { // TODO: Use $self to navigate to struct | ||
if (_art?.elements && !validStructuredElement && $scope !== '$self') { // TODO: Use $self to navigate to struct | ||
this.error(null, expression[i].$path, { elemref: { ref } }, | ||
@@ -27,0 +27,0 @@ 'Unexpected usage of structured type $(ELEMREF)'); |
@@ -6,3 +6,4 @@ 'use strict'; | ||
* Make sure that all source artifacts and association targets reach the database | ||
* (otherwise the view can't be activated), but only if the source artifact is NOT activated against the database | ||
* (otherwise the view can't be activated), but only if the source artifact is NOT activated against the database // <- what does this mean? | ||
* | ||
* Check the given query for: | ||
@@ -9,0 +10,0 @@ * - Associations-traversal over skipped/abstract things |
@@ -8,5 +8,9 @@ 'use strict'; | ||
/** | ||
* Validate select items of a query. | ||
* Validate select items of a query. If a column reference starts with $self or $projection, it must not contain association steps. | ||
* Furthermore, for to.hdbcds, window functions are not allowed. | ||
* | ||
* For to.hdbcds-hdbcds, structures and managed associations are not allowed as they are not flattened - @see rejectManagedAssociationsAndStructuresForHdbcdsNames | ||
* | ||
* @param {CSN.Query} query query object | ||
* @todo Why do we care about this with $self? | ||
*/ | ||
@@ -13,0 +17,0 @@ function validateSelectItems(query) { |
@@ -132,4 +132,4 @@ 'use strict'; | ||
const { getFinalBaseType } = getUtils(model); | ||
const typeOfType = getFinalBaseType(artOrElement.type, path); | ||
const { getFinalBaseTypeWithProps } = getUtils(model); | ||
const typeOfType = getFinalBaseTypeWithProps(artOrElement.type); | ||
@@ -161,2 +161,3 @@ if (typeOfType === null) { | ||
* @param {boolean} isElement indicates whether we are dealing with an element or an artifact | ||
* @todo Rename, is an error not a warning | ||
*/ | ||
@@ -178,2 +179,3 @@ function warnAboutMissingType(error, path, name, isElement = false) { | ||
* @returns {boolean} indicates whether the artifact has type information | ||
* @todo What is the point of isBuiltinType here if we check for artifact.type at the end? | ||
*/ | ||
@@ -180,0 +182,0 @@ function hasArtifactTypeInformation(artifact) { |
@@ -34,3 +34,3 @@ 'use strict'; | ||
/** | ||
* Artifact is structured or a managed association/compoisition | ||
* Artifact is structured or a managed association/composition | ||
* | ||
@@ -53,3 +53,3 @@ * @param {CSN.Artifact} art Artifact | ||
if (art && art.type && !isBuiltinType(art.type)) | ||
return this.getFinalBaseType(art); | ||
return this.csnUtils.getFinalBaseTypeWithProps(art.type); | ||
@@ -56,0 +56,0 @@ return art; |
@@ -106,4 +106,5 @@ 'use strict'; | ||
// TODO: checkManagedAssoc is a forEachMemberRecursively! | ||
const commonArtifactValidators = [ checkTypeDefinitionHasType, checkPrimaryKey, checkManagedAssoc ]; | ||
// TODO: Does it make sense to run the on-condition check as part of a CSN validator? | ||
const commonQueryValidators = [ validateMixinOnCondition ]; | ||
@@ -110,0 +111,0 @@ |
@@ -121,4 +121,6 @@ // Consistency checker on model (XSN = augmented CSN) | ||
}, | ||
location: { // location req if at least one property: | ||
isRequired: parent => noSyntaxErrors() || Object.keys( parent ).length, | ||
location: { | ||
// every thing with a $location in CSN must have a XSN location even | ||
// with syntax errors (currently even internal artifacts like $using): | ||
isRequired: parent => noSyntaxErrors() || parent && parent.kind, | ||
kind: true, | ||
@@ -189,3 +191,6 @@ requires: [ 'file' ], // line is optional in top-level location | ||
requires: [ 'kind', 'name' ], | ||
optional: [ 'elements', '$autoElement', '$uncheckedElements', '_effectiveType', '_deps' ], | ||
optional: [ | ||
'elements', '$autoElement', '$uncheckedElements', | ||
'$requireElementAccess', '_effectiveType', '_deps', | ||
], | ||
schema: { | ||
@@ -196,2 +201,3 @@ kind: { test: isString, enum: [ 'builtin' ] }, | ||
$uncheckedElements: { test: isBoolean }, | ||
$requireElementAccess: { test: isBoolean }, | ||
// missing location for normal "elements" | ||
@@ -223,4 +229,3 @@ elements: { test: TODO }, | ||
requires: [ 'kind', 'location' ], | ||
optional: [ 'name', 'extern', 'usings', '$annotations', 'fileDep' ], | ||
// TODO: get rid of $annotations: [] | ||
optional: [ 'name', 'extern', 'usings', 'fileDep' ], | ||
}, | ||
@@ -311,3 +316,2 @@ extern: { | ||
requires: [ 'location', 'name' ], | ||
optional: [ '$annotations' ], // TODO: get rid of annos: [] | ||
}, | ||
@@ -457,7 +461,7 @@ orderBy: { inherits: 'value', test: isArray( expression ) }, | ||
inherits: 'value', | ||
optional: [ 'name', '_block', '$priority', '$duplicate', '$inferred', '$duplicates' ], | ||
optional: [ 'name', '_block', '$priority', '$inferred', '$duplicates', '$errorReported' ], | ||
// TODO: name requires if not in parser? | ||
}, | ||
$priority: { test: TODO }, // TODO: rename to $priority | ||
$annotations: { parser: true, kind: true, test: TODO }, | ||
$priority: { test: TODO }, | ||
$annotations: { parser: true, kind: true, test: TODO }, // deprecated, still there for cds-lsp | ||
name: { | ||
@@ -477,3 +481,3 @@ isRequired: stageParser && (() => false), // not required in parser | ||
absolute: { test: isString }, | ||
variant: { test: TODO }, // TODO: not set in CDL parser, only in $annotations | ||
variant: { test: TODO }, // TODO: not set in CDL parser | ||
element: { test: TODO }, // TODO: { test: isString }, | ||
@@ -591,2 +595,3 @@ action: { test: isString }, | ||
$errorReported: { parser: true, test: isBoolean }, // to avoid duplicate messages | ||
$duplicates: { parser: true, kind: true, test: TODO }, // array of arts or true | ||
@@ -593,0 +598,0 @@ $extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status |
@@ -79,16 +79,89 @@ // The builtin artifacts of CDS | ||
const specialFunctions = { | ||
const specialFunctions = compileFunctions( { | ||
'': [ // the default | ||
{ | ||
intro: [ 'ALL', 'DISTINCT' ], | ||
introMsg: [], // do not list them in code completion | ||
}, | ||
{}, | ||
], | ||
ROUND: [ | ||
null, null, { // 3rd argument: rounding mode | ||
ROUND_HALF_UP: 'argFull', | ||
ROUND_HALF_DOWN: 'argFull', | ||
ROUND_HALF_EVEN: 'argFull', | ||
ROUND_UP: 'argFull', | ||
ROUND_DOWN: 'argFull', | ||
ROUND_CEILING: 'argFull', | ||
ROUND_FLOOR: 'argFull', | ||
expr: [ 'ROUND_HALF_UP', 'ROUND_HALF_DOWN', 'ROUND_HALF_EVEN', | ||
'ROUND_UP', 'ROUND_DOWN', 'ROUND_CEILING', 'ROUND_FLOOR' ], | ||
}, | ||
], | ||
}; | ||
TRIM: [ | ||
{ | ||
intro: [ 'LEADING', 'TRAILING', 'BOTH' ], | ||
expr: [ 'LEADING', 'TRAILING', 'BOTH' ], | ||
separator: [ 'FROM' ], | ||
}, | ||
], | ||
EXTRACT: [ | ||
{ | ||
expr: [ 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND' ], | ||
separator: [ 'FROM' ], | ||
}, | ||
], | ||
COUNT: [ | ||
{ | ||
expr: [ '*' ], | ||
intro: [ 'ALL', 'DISTINCT' ], | ||
}, | ||
], | ||
MIN: 'COUNT', | ||
MAX: 'COUNT', | ||
SUM: 'COUNT', | ||
AVG: 'COUNT', | ||
STDDDEV: 'COUNT', | ||
VAR: 'COUNT', | ||
LOCATE_REGEXPR: [ | ||
{ | ||
intro: [ 'START', 'AFTER' ], | ||
separator: [ 'FLAG', 'IN', 'FROM', 'OCCURRENCE', 'GROUP' ], | ||
}, | ||
], | ||
OCCURRENCES_REGEXPR: [ | ||
{ | ||
separator: [ 'FLAG', 'IN', 'FROM' ], | ||
}, | ||
], | ||
REPLACE_REGEXPR: [ | ||
{ | ||
separator: [ 'FLAG', 'IN', 'WITH', 'FROM', 'OCCURRENCE' ], | ||
expr: [ 'ALL' ], | ||
}, | ||
], | ||
SUBSTRING_REGEXPR: [ | ||
{ | ||
separator: [ 'FLAG', 'IN', 'FROM', 'OCCURRENCE', 'GROUP' ], | ||
}, | ||
], | ||
} ); | ||
function compileFunctions( special ) { | ||
const compiled = {}; | ||
for (const [ name, val ] of Object.entries( special )) | ||
compiled[name] = (typeof val === 'string' ? special[val] : val).map( compileArg ); | ||
return compiled; | ||
} | ||
function compileArg( src ) { | ||
if (!src) | ||
return src; | ||
const tgt = { | ||
intro: src.intro || [], | ||
expr: src.expr || [], | ||
separator: src.separator || [], | ||
}; | ||
for (const generic of [ 'intro', 'expr', 'separator' ]) { // intro before expr! | ||
for (const token of src[generic] || []) | ||
tgt[token] = generic; | ||
} | ||
if (tgt.intro) // same token could be in both 'expr' and 'intro': | ||
tgt.introMsg = src.introMsg || tgt.intro.filter( token => tgt[token] === 'intro' ); | ||
return tgt; | ||
} | ||
/** | ||
@@ -110,2 +183,4 @@ * Variables that have special meaning in CDL/CSN. | ||
}, | ||
// Require that elements are accessed, i.e. no $at, only $at.<element>. | ||
$requireElementAccess: true, | ||
}, | ||
@@ -117,2 +192,3 @@ $now: {}, // Dito | ||
$uncheckedElements: true, | ||
$requireElementAccess: true, | ||
}, | ||
@@ -292,2 +368,4 @@ }; | ||
art.$uncheckedElements = magic.$uncheckedElements; | ||
if (magic.$requireElementAccess) | ||
art.$requireElementAccess = magic.$requireElementAccess; | ||
@@ -294,0 +372,0 @@ createMagicElements( art, magic.elements ); |
@@ -508,3 +508,3 @@ // Compiler phase "define": transform dictionary of AST-like CSNs into augmented CSN | ||
if (art.kind !== 'namespace' && | ||
isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' )) { | ||
isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' )) { | ||
let p = parent; | ||
@@ -515,3 +515,3 @@ while (p && kindProperties[p.kind].artifacts) | ||
error( 'subartifacts-not-supported', [ art.name.location, art ], | ||
{ art: p, prop: 'deprecated.generatedEntityNameWithUnderscore' }, | ||
{ art: p, prop: 'deprecated._generatedEntityNameWithUnderscore' }, | ||
// eslint-disable-next-line max-len | ||
@@ -518,0 +518,0 @@ 'With the option $(PROP), no sub artifact can be defined for a non-context/service $(ART)' ); |
@@ -10,4 +10,4 @@ // Extend, include, localized data and managed compositions | ||
const { | ||
isDeprecatedEnabled, isBetaEnabled, | ||
forEachGeneric, forEachInOrder, | ||
isDeprecatedEnabled, | ||
forEachGeneric, forEachInOrder, forEachDefinition, | ||
} = require('../base/model'); | ||
@@ -51,5 +51,4 @@ const { dictAdd } = require('../base/dictionaries'); | ||
const commonLanguagesEntity // TODO: remove beta after a grace period | ||
= (options.addTextsLanguageAssoc || isBetaEnabled( options, 'addTextsLanguageAssoc' )) && | ||
model.definitions['sap.common.Languages']; | ||
const commonLanguagesEntity = options.addTextsLanguageAssoc && | ||
model.definitions['sap.common.Languages']; | ||
const addTextsLanguageAssoc = !!(commonLanguagesEntity && commonLanguagesEntity.elements && | ||
@@ -61,2 +60,4 @@ commonLanguagesEntity.elements.code); | ||
compositionChildPersistence(); | ||
/** | ||
@@ -94,2 +95,21 @@ * Process "composition of" artifacts. | ||
/** | ||
* Copy `@cds.persistence.skip` and `@cds.persistence.skip` from parent to child | ||
* for managed compositions. This needs to be done after extensions, i.e. annotations, | ||
* have been applied or `annotate E.comp` would not have an effect on `E.comp.subComp`. | ||
*/ | ||
function compositionChildPersistence() { | ||
const processed = new WeakSet(); | ||
forEachDefinition(model, processCompositionPersistence); | ||
function processCompositionPersistence(def) { | ||
if (def.$inferred === 'composition-entity' && !processed.has(def)) { | ||
if (def._parent) | ||
processCompositionPersistence(def._parent); | ||
copyPersistenceAnnotations(def, def._parent, options); | ||
processed.add(def); | ||
} | ||
} | ||
} | ||
// extend ------------------------------------------------------------------ | ||
@@ -499,3 +519,3 @@ | ||
const textsName = (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' )) | ||
const textsName = (isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' )) | ||
? `${ art.name.absolute }_texts` | ||
@@ -509,4 +529,5 @@ : `${ art.name.absolute }.texts`; | ||
return; // -> make it idempotent | ||
createTextsEntity( art, textsName, localized, fioriEnabled ); | ||
const newTextsEntity = createTextsEntity( art, textsName, localized, fioriEnabled ); | ||
addTextsAssociations( art, textsName, localized ); | ||
copyPersistenceAnnotations(newTextsEntity, art, options); | ||
} | ||
@@ -645,3 +666,3 @@ | ||
} | ||
if (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' )) | ||
if (isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' )) | ||
setLink( art, '_base', base ); | ||
@@ -705,2 +726,4 @@ | ||
annotateWith( art, '@assert.unique.locale', art.location, assertUniqueValue, 'array' ); | ||
return art; | ||
} | ||
@@ -819,3 +842,3 @@ | ||
return; | ||
const entityName = (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' )) | ||
const entityName = (isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' )) | ||
? `${ base.name.absolute }_${ elem.name.id }` | ||
@@ -943,3 +966,3 @@ : `${ base.name.absolute }.${ elem.name.id }`; | ||
// primary keys and add the ON condition | ||
if (isDeprecatedEnabled( options, 'unmanagedUpInComponent' )) { | ||
if (isDeprecatedEnabled( options, '_unmanagedUpInComponent' )) { | ||
addProxyElements( art, keys, 'aspect-composition', target.name && location, | ||
@@ -955,3 +978,3 @@ 'up__', '@odata.containment.ignore' ); | ||
} | ||
if (isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' )) | ||
if (isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' )) | ||
setLink( art, '_base', base._base || base ); | ||
@@ -965,2 +988,6 @@ | ||
initArtifact( art ); | ||
// Copy persistence annotations from aspect. | ||
copyPersistenceAnnotations(art, target, options); | ||
return art; | ||
@@ -987,2 +1014,23 @@ } | ||
/** | ||
* Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from | ||
* source to target if present on source but not target. | ||
* | ||
* @param {object} target | ||
* @param {object} source | ||
* @param {CSN.Options} options | ||
*/ | ||
function copyPersistenceAnnotations(target, source, options) { | ||
if (!source) | ||
return; | ||
// Copy @cds.persistence.skip/exists annotation. | ||
const noCopyExists = isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' ); | ||
const existsAnno = '@cds.persistence.exists'; | ||
const skipAnno = '@cds.persistence.skip'; | ||
if (!noCopyExists && source[existsAnno] && !target[existsAnno]) | ||
target[existsAnno] = source[existsAnno]; | ||
if (source[skipAnno] && !target[skipAnno]) | ||
target[skipAnno] = source[skipAnno]; | ||
} | ||
function augmentEqual( location, assocname, relations, prefix = '' ) { | ||
@@ -989,0 +1037,0 @@ const args = relations.map( eq ); |
@@ -5,2 +5,3 @@ // Things which needs to done for parse.cdl after define() | ||
const { dictAddArray } = require('../base/dictionaries'); | ||
const { forEachGeneric, forEachMember } = require('../base/model'); | ||
@@ -37,3 +38,3 @@ const { setLink, setArtifactLink } = require('./utils'); | ||
// Define annotations of this top-level extension | ||
defineAnnotations( ext, ext, ext._block ); | ||
defineAnnotations( ext, ext, ext._block, 'extend' ); | ||
mergeAnnotatesForSameArtifact( ext ); | ||
@@ -226,15 +227,25 @@ // Initialize members and define annotations in sub-elements. | ||
if (ext.$annotations && Array.isArray(ext.$duplicates)) { | ||
const annotates = ext.$duplicates.filter(val => (val.kind === 'annotate')); | ||
for (const dup of annotates) { | ||
ext.$annotations.push(...dup.$annotations); | ||
delete dup.$annotations; | ||
// do not do a complex merge: | ||
if (isComplexExtension( ext ) || | ||
!Array.isArray( ext.$duplicates ) || ext.$duplicates.some( isComplexExtension )) | ||
return; | ||
for (const dup of ext.$duplicates) { | ||
for (const prop in dup) { | ||
if (prop.charAt(0) === '@') | ||
dictAddArray( ext, prop, dup[prop] ); | ||
} | ||
ext.$duplicates = ext.$duplicates.filter(val => (val.kind !== 'annotate')); | ||
if (ext.$duplicates.length === 0) | ||
delete ext.$duplicates; | ||
} | ||
delete ext.$duplicates; | ||
} | ||
} | ||
/** | ||
* We only de-duplicate an extend/annotate `ext` in function | ||
* mergeAnnotatesForSameArtifact() if the extend/annotate is simple, i.e. has | ||
* no members like elements. | ||
*/ | ||
function isComplexExtension( ext ) { | ||
return ext.kind !== 'annotate' || ext.elements || ext.parameters || ext.actions; | ||
} | ||
module.exports = finalizeParseCdl; |
@@ -36,2 +36,3 @@ // Main XSN-based compiler functions | ||
const { createMessageFunctions, deduplicateMessages } = require('../base/messages'); | ||
const { checkRemovedDeprecatedFlags } = require('../base/model'); | ||
const { promiseAllDoNotRejectImmediately } = require('../base/node-helpers'); | ||
@@ -228,2 +229,3 @@ const { cdsFs } = require('../utils/file'); | ||
* @param {object} [options={}] Compilation options. | ||
* @param {object} [fileCache] | ||
* @returns {XSN.Model} Augmented CSN | ||
@@ -240,3 +242,3 @@ */ | ||
let asts = []; | ||
const asts = []; | ||
const errors = []; | ||
@@ -259,6 +261,5 @@ a.files.forEach( val => readAndParseSync( val, (err, ast) => { | ||
const fileNames = readDependenciesSync( asts ); | ||
asts = []; | ||
// TODO: check the following eslint error | ||
// eslint-disable-next-line no-loop-func | ||
fileNames.forEach( (fileName) => { | ||
asts.length = 0; | ||
// Push dependencies to `ast`. Only works because readAndParseSync() is synchronous. | ||
for (const fileName of fileNames) { | ||
readAndParseSync(fileName, ( err, ast ) => { | ||
@@ -270,3 +271,3 @@ if (err) | ||
}); | ||
} ); | ||
} | ||
} | ||
@@ -386,2 +387,11 @@ } | ||
} | ||
for (const dep of sources[filename].dependencies || []) { | ||
if (!dep.realname) { | ||
// `realname` is used by setLayers(). For compileSources(), we don't resolve | ||
// the USING paths and use the literal instead, which may be part of the | ||
// source dictionary. | ||
dep.realname = dep.val; | ||
} | ||
} | ||
} | ||
@@ -400,10 +410,11 @@ moduleLayers.setLayers( sources ); | ||
* | ||
* TODO: probaby issue message api-recompiled-csn there. | ||
* TODO: probably issue message api-recompiled-csn there. | ||
*/ | ||
function recompileX( csn, options ) { | ||
// Explicitly set parseCdl to false because backends cannot handle it | ||
options = { ...options, parseCdl: false, $recompile: true }; | ||
// Reset csnFlavor: Use client style (default) | ||
delete options.csnFlavor; | ||
delete options.toCsn; | ||
// TODO: $recompile: true should be enough | ||
// Explicitly set parseCdl to false because backends cannot handle it | ||
// Explicitly delete all toCsn options: | ||
delete options.toCsn; | ||
@@ -438,2 +449,5 @@ const file = csn.$location && csn.$location.file && | ||
model.meta = {}; // provide initial central meta object | ||
checkRemovedDeprecatedFlags( options, model.$messageFunctions ); | ||
if (options.parseOnly) { | ||
@@ -511,3 +525,3 @@ throwWithError(); | ||
// already handles non-existent files. | ||
name = fs.realpathSync(name); | ||
name = fs.realpathSync.native(name); | ||
} | ||
@@ -514,0 +528,0 @@ catch (e) { |
@@ -56,2 +56,8 @@ // Module handling, layers and packages | ||
function realname( art ) { | ||
while (art && art.kind !== 'source') | ||
art = art._block; | ||
return art && art.realname || ''; | ||
} | ||
function compareLayer( a, b ) { | ||
@@ -68,3 +74,4 @@ while (a && a.kind !== 'source') | ||
layer, | ||
realname, | ||
compareLayer, | ||
}; |
@@ -77,3 +77,3 @@ // Populate views with elements, elements with association targets, ... | ||
// behavior depending on option `deprecated`: | ||
const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' ); | ||
const enableExpandElements = !isDeprecatedEnabled( options, '_noElementsExpansion' ); | ||
// TODO: we should get rid of noElementsExpansion soon; both | ||
@@ -83,12 +83,12 @@ // beta.nestedProjections and beta.universalCsn do not work with it. | ||
= enableExpandElements && | ||
!isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ) && | ||
!isDeprecatedEnabled( options, 'shortAutoexposed' ) && | ||
!isDeprecatedEnabled( options, 'longAutoexposed' ) && | ||
!isDeprecatedEnabled( options, 'noInheritedAutoexposeViaComposition' ) && | ||
!isDeprecatedEnabled( options, 'noScopedRedirections' ); | ||
!isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' ) && | ||
!isDeprecatedEnabled( options, '_shortAutoexposed' ) && | ||
!isDeprecatedEnabled( options, '_longAutoexposed' ) && | ||
!isDeprecatedEnabled( options, '_noInheritedAutoexposeViaComposition' ) && | ||
!isDeprecatedEnabled( options, '_noScopedRedirections' ); | ||
const autoexposeViaComposition | ||
= (isDeprecatedEnabled( options, 'noInheritedAutoexposeViaComposition' )) | ||
= (isDeprecatedEnabled( options, '_noInheritedAutoexposeViaComposition' )) | ||
? 'Composition' | ||
: true; | ||
const redirectInSubQueries = isDeprecatedEnabled( options, 'redirectInSubQueries' ); | ||
const redirectInSubQueries = isDeprecatedEnabled( options, '_redirectInSubQueries' ); | ||
@@ -1099,3 +1099,3 @@ forEachDefinition( model, traverseElementEnvironments ); | ||
// no @cds.autoexpose or @cds.autoexpose:null | ||
// TODO: introduce deprecated.noInheritedAutoexposeViaComposition | ||
// TODO: introduce deprecated._noInheritedAutoexposeViaComposition | ||
art.$autoexpose = model.$compositionTargets[art.name.absolute] | ||
@@ -1109,11 +1109,11 @@ ? autoexposeViaComposition | ||
const { absolute } = target.name; | ||
if (isDeprecatedEnabled( options, 'shortAutoexposed' )) { | ||
if (isDeprecatedEnabled( options, '_shortAutoexposed' )) { | ||
const parent = definitionScope( target )._parent; | ||
const name = (parent) ? absolute.substring( parent.name.absolute.length + 1 ) : absolute; | ||
// no need for dedot here (as opposed to deprecated.longAutoexposed), as | ||
// no need for dedot here (as opposed to deprecated._longAutoexposed), as | ||
// the name for dependent entities have already been created using `_` then | ||
return `${ service.name.absolute }.${ name }`; | ||
} | ||
if (isDeprecatedEnabled( options, 'longAutoexposed' )) { | ||
const dedot = isDeprecatedEnabled( options, 'generatedEntityNameWithUnderscore' ); | ||
if (isDeprecatedEnabled( options, '_longAutoexposed' )) { | ||
const dedot = isDeprecatedEnabled( options, '_generatedEntityNameWithUnderscore' ); | ||
return `${ service.name.absolute }.${ dedot ? absolute.replace( /\./g, '_' ) : absolute }`; | ||
@@ -1120,0 +1120,0 @@ } |
@@ -59,5 +59,5 @@ // | ||
const { options } = model; | ||
const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' ); | ||
const enableExpandElements = !isDeprecatedEnabled( options, '_noElementsExpansion' ); | ||
// eslint-disable-next-line max-len | ||
const oldVirtualNotNullPropagation = isDeprecatedEnabled( options, 'oldVirtualNotNullPropagation' ); | ||
const oldVirtualNotNullPropagation = isDeprecatedEnabled( options, '_oldVirtualNotNullPropagation' ); | ||
@@ -193,3 +193,3 @@ forEachDefinition( model, run ); | ||
// We do not consider the $expand status, as elements are already expanded | ||
// by the resolve(), and if not due to deprecated.noElementsExpansion | ||
// by the resolve(), and if not due to deprecated._noElementsExpansion | ||
run( type ); | ||
@@ -196,0 +196,0 @@ return type[prop]; |
@@ -52,3 +52,2 @@ // Compiler phase "resolve": resolve all references | ||
const { combinedLocation } = require('../base/location'); | ||
const { forEachValue } = require('../utils/objectUtils'); | ||
const { typeParameters } = require('./builtins'); | ||
@@ -60,2 +59,3 @@ | ||
setArtifactLink, | ||
annotationHasEllipsis, | ||
pathName, | ||
@@ -78,5 +78,2 @@ linkToOrigin, | ||
const annotationPriorities = { | ||
define: 1, extend: 2, annotate: 2, edmx: 3, | ||
}; | ||
const $inferred = Symbol.for('cds.$inferred'); | ||
@@ -113,3 +110,3 @@ | ||
// behavior depending on option `deprecated`: | ||
const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' ); | ||
const enableExpandElements = !isDeprecatedEnabled( options, '_noElementsExpansion' ); | ||
// TODO: we should get rid of noElementsExpansion soon; both | ||
@@ -218,5 +215,5 @@ // beta.nestedProjections and beta.universalCsn do not work with it. | ||
// eslint-disable-next-line max-len | ||
std: 'Selecting from to-many association $(ART) - key properties are not propagated', | ||
std: 'Key properties are not propagated because a to-many association $(ART) is selected', | ||
// eslint-disable-next-line max-len | ||
element: 'Selecting from to-many association $(MEMBER) of $(ART) - key properties are not propagated', | ||
element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected', | ||
} ); | ||
@@ -722,237 +719,215 @@ } | ||
function chooseAssignment( annoName, art ) { | ||
// TODO: getPath an all names | ||
const anno = art[annoName]; | ||
if (!Array.isArray(anno)) { // just one assignment -> use it | ||
if (removeEllipsis( anno )) { | ||
error( 'anno-unexpected-ellipsis', | ||
[ anno.name.location, art ], { code: '...' } ); | ||
let anno = art[annoName]; | ||
if (!Array.isArray( anno )) { // just one assignment -> use it | ||
if (!annotationHasEllipsis( anno )) | ||
return; | ||
anno = [ anno ]; | ||
} | ||
// console.log('ASSIGN:',art.name.absolute,annoName) | ||
const scheduledAssignments = []; | ||
// sort assignment according to layer (define is bottom layer): | ||
const layeredAnnos = layeredAssignments( anno ); | ||
let cont = true; | ||
while (cont) { | ||
const { assignments, issue } = assignmentsOfHighestLayers( layeredAnnos ); | ||
let index = assignments.length; | ||
cont = !!index; // safety | ||
while (--index >= 0) { | ||
const a = assignments[index]; | ||
scheduledAssignments.push( a ); | ||
if (!annotationHasEllipsis( a )) { | ||
cont = false; | ||
break; | ||
} | ||
} | ||
return; | ||
if (issue) { | ||
// eslint-disable-next-line no-nested-ternary | ||
const msg = (issue === true) | ||
? 'anno-duplicate' | ||
: (index >= 0) ? 'anno-duplicate-unrelated-layer' : 'anno-unstable-array'; | ||
for (const a of assignments) { | ||
if (!a.$errorReported) | ||
message( msg, [ a.name.location, art ], { anno: annoName } ); | ||
} | ||
} | ||
// else if (index > 0) -- if we allow multiple assignments in one file - the last wins | ||
} | ||
// sort assignment according to layer | ||
const layerAnnos = Object.create(null); | ||
for (const a of anno) { | ||
const layer = layers.layer( a._block ); | ||
// Now apply the assignments - all but the first have a '...' | ||
let result = null; | ||
scheduledAssignments.reverse(); | ||
for (const a of scheduledAssignments) | ||
result = applyAssignment( result, a, art, annoName ); | ||
art[annoName] = result.name ? result | ||
: Object.assign( {}, scheduledAssignments[scheduledAssignments.length - 1], result ); | ||
} | ||
// Group assignments by their layers. An assignment provided with a definition | ||
// is considered to be provided in a layer named '', the lowest layer. | ||
// TODO: make this usable for extend (elements), too = | ||
// do not use $priority, make assignments on define do not have own _block | ||
function layeredAssignments( assignment ) { | ||
const layered = Object.create(null); | ||
for (const a of assignment) { | ||
const layer = a.$priority && layers.layer( a ); | ||
// just consider layer if Extend/Annotate, not Define | ||
const name = (layer) ? layer.realname : ''; | ||
const done = layerAnnos[name]; | ||
const done = layered[name]; | ||
if (done) | ||
done.annos.push( a ); | ||
done.assignments.push( a ); | ||
else | ||
layerAnnos[name] = { layer, annos: [ a ] }; | ||
layered[name] = { name, layer, assignments: [ a ] }; | ||
// TODO: file - if set: unique in layer | ||
} | ||
mergeArrayInSCCs(); | ||
art[annoName] = mergeLayeredArrays( findLayerCandidate( ) ); | ||
return; | ||
return layered; | ||
} | ||
// Merge annotations in each layer, i.e. multiple annotations in the same layer are | ||
// stored in an array and need to be merged before different layers can be merged. | ||
function mergeArrayInSCCs( ) { | ||
let pos = 0; | ||
forEachValue(layerAnnos, (layer) => { | ||
const mergeSource = layer.annos.find(v => (v.$priority === undefined || | ||
annotationPriorities[v.$priority] === annotationPriorities.define)); | ||
if (mergeSource) { | ||
// If the source annotation (at 'define' level) contains an ellipsis, | ||
// there is no base to apply to. | ||
if (removeEllipsis( mergeSource )) { | ||
error( 'anno-unexpected-ellipsis', | ||
[ mergeSource.name.location, art ], { code: '...' } ); | ||
} | ||
// merge source into ellipsis array annotates | ||
layer.annos.forEach( (mergeTarget) => { | ||
if (mergeTarget.$priority && | ||
annotationPriorities[mergeTarget.$priority] > annotationPriorities.define) { | ||
pos = findEllipsis( mergeTarget ); | ||
if (pos > -1) { | ||
if (mergeSource.literal !== 'array') { | ||
error( 'anno-mismatched-ellipsis', | ||
[ mergeSource.name.location, art ], { code: '...' } ); | ||
return; | ||
} | ||
mergeTarget.val = mergeArrayValues( mergeSource.val, mergeTarget.val ); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
// Return assignments of the highest layers. | ||
// Also return whether there could be an issue: | ||
// - false: there is just one assignment | ||
// - 'unrelated': there is just one assignment per layer | ||
// - true: there is at least one layer with two or more assignments | ||
// TODO: make this usable for extend (elements), too | ||
function assignmentsOfHighestLayers( layeredAnnos ) { | ||
const layerNames = Object.keys( layeredAnnos ); | ||
// console.log('HIB:',layerNames) | ||
if (layerNames.length <= 1) { | ||
const name = layerNames[0]; | ||
const { assignments } = layeredAnnos[name] || { assignments: [] }; | ||
delete layeredAnnos[name]; | ||
return { assignments, issue: assignments.length > 1 }; | ||
} | ||
function mergeLayeredArrays( mergeTarget ) { | ||
if (mergeTarget.literal === 'array') { | ||
let layer = layers.layer( mergeTarget._block ); | ||
delete layerAnnos[(layer) ? layer.realname : '']; | ||
let pos = findEllipsis( mergeTarget ); | ||
let hasRun = false; | ||
while (pos > -1 && Object.keys( layerAnnos ).length ) { | ||
hasRun = true; | ||
const mergeSource = findLayerCandidate(); | ||
if (mergeSource.literal !== 'array') { | ||
error( 'anno-mismatched-ellipsis', | ||
[ mergeSource.name.location, art ], { code: '...' } ); | ||
return mergeTarget; | ||
} | ||
mergeTarget.val = mergeArrayValues( mergeSource.val, mergeTarget.val ); | ||
layer = layers.layer( mergeSource._block ); | ||
delete layerAnnos[(layer) ? layer.realname : '']; | ||
pos = findEllipsis( mergeTarget ); | ||
} | ||
// All layers were processed. Remove excess ellipsis. | ||
if (removeEllipsis( mergeTarget, pos ) && hasRun) { | ||
// There shouldn't be any ellipsis or we don't have a base annotation. | ||
// But only if the loop above has run. Otherwise the in-layer merge | ||
// already warned about this case. | ||
message( 'anno-unexpected-ellipsis-layers', | ||
[ mergeTarget.name.location, art ], { code: '...' } ); | ||
} | ||
// collect all layers which are lower than another layer | ||
const allExtends = Object.create(null); | ||
allExtends[''] = {}; // the "Define" layer | ||
for (const name of layerNames) { | ||
if (name) // not the "Define" layer | ||
Object.assign( allExtends, layeredAnnos[name].layer._layerExtends ); | ||
} | ||
// console.log('HIE:',Object.keys(allExtends)) | ||
const assignments = []; | ||
const highest = []; | ||
for (const name of layerNames) { | ||
if (!(name in allExtends)) { | ||
const layer = layeredAnnos[name]; | ||
delete layeredAnnos[name]; | ||
highest.push( layer ); | ||
assignments.push( ...layer.assignments ); | ||
} | ||
return mergeTarget; | ||
} | ||
assignments.sort( compareAssignments ); | ||
const good = highest.every( layer => layer.assignments.length === 1 ); | ||
// TODO: use layer.file instead | ||
const issue = !good || highest.length > 1 && 'unrelated'; | ||
// console.log('HI:',highest.map(l=>l.name),issue,issue&&assignments) | ||
return { assignments, issue }; | ||
} | ||
function mergeArrayValues( previousValue, arraySpec ) { | ||
let prevPos = 0; | ||
const result = []; | ||
for (const item of arraySpec) { | ||
const ell = item && item.literal === 'token' && item.val === '...'; | ||
if (!ell) { | ||
result.push( item ); | ||
} | ||
else { | ||
let upToSpec = item.upTo && checkUpToSpec( item.upTo, true ); | ||
while (prevPos < previousValue.length) { | ||
const prevItem = previousValue[prevPos++]; | ||
result.push( prevItem ); | ||
if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) { | ||
upToSpec = false; | ||
break; | ||
} | ||
function compareAssignments( a, b ) { | ||
const fileA = layers.realname( a._block ); | ||
const fileB = layers.realname( b._block ); | ||
if (fileA !== fileB) | ||
return (fileA > fileB) ? 1 : -1; | ||
return (a?.location?.line || 0) - (b?.location?.line || 0) || | ||
(a?.location?.col || 0) - (b?.location?.col || 0); | ||
} | ||
function applyAssignment( previousAnno, anno, art, annoName ) { | ||
if (!previousAnno) { | ||
if (!annotationHasEllipsis( anno )) | ||
return anno; | ||
if (anno.$priority) { // already complained about with Define | ||
message( 'anno-unexpected-ellipsis-layers', // TODO: better location | ||
[ anno.name.location, art ], { code: '...' } ); | ||
} | ||
previousAnno = { val: [] }; | ||
} | ||
else if (previousAnno.literal !== 'array') { | ||
error( 'anno-mismatched-ellipsis', // TODO: better location | ||
[ anno.name.location, art ], { code: '...' } ); | ||
previousAnno = { val: [] }; | ||
} | ||
const previousValue = previousAnno.val; | ||
let prevPos = 0; | ||
const result = []; | ||
for (const item of anno.val) { | ||
const ell = item && item.literal === 'token' && item.val === '...'; | ||
if (!ell) { | ||
result.push( item ); | ||
} | ||
else { | ||
let upToSpec = item.upTo && checkUpToSpec( item.upTo, art, annoName, true ); | ||
while (prevPos < previousValue.length) { | ||
const prevItem = previousValue[prevPos++]; | ||
result.push( prevItem ); | ||
if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) { | ||
upToSpec = false; | ||
break; | ||
} | ||
if (upToSpec) { // non-matched UP TO | ||
warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' }, | ||
'The $(CODE) value does not match any item in the base annotation $(ANNO)' ); | ||
} | ||
} | ||
if (upToSpec) { // non-matched UP TO | ||
warning( null, [ item.upTo.location, art ], { anno: annoName, code: '... up to' }, | ||
'The $(CODE) value does not match any item in the base annotation $(ANNO)' ); | ||
} | ||
} | ||
return result; | ||
} | ||
// console.log('TP:',previousValue.map(se),anno.val.map(se),'->',result.map(se)) | ||
return { val: result, literal: 'array' }; | ||
} | ||
// function se(a) { return a.upTo ? [a.val,a.upTo.val] : a.val ; } | ||
function checkUpToSpec( upToSpec, trueIfFullUpTo ) { | ||
const { literal } = upToSpec; | ||
if (trueIfFullUpTo !== true) { // inside struct of UP TO | ||
if (literal !== 'struct' && literal !== 'array' ) | ||
return true; | ||
} | ||
else if (literal === 'struct') { | ||
return Object.values( upToSpec.struct ).every( checkUpToSpec ); | ||
} | ||
else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') { | ||
function checkUpToSpec( upToSpec, art, annoName, isFullUpTo ) { | ||
const { literal } = upToSpec; | ||
if (!isFullUpTo) { // inside struct of UP TO | ||
if (literal !== 'struct' && literal !== 'array' ) | ||
return true; | ||
} | ||
error( null, [ upToSpec.location, art ], | ||
{ anno: annoName, code: '... up to', '#': literal }, | ||
{ | ||
std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)', | ||
array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)', | ||
// eslint-disable-next-line max-len | ||
struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)', | ||
boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)', | ||
null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)', | ||
} ); | ||
return false; | ||
} | ||
function equalUpTo( previousItem, upToSpec ) { | ||
if (!previousItem) | ||
return false; | ||
if ('val' in upToSpec) { | ||
if (previousItem.val === upToSpec.val) // enum, struct and ref have no val | ||
return true; | ||
const typeUpTo = typeof upToSpec.val; | ||
const typePrev = typeof previousItem.val; | ||
if (typeUpTo === 'number') | ||
return typePrev === 'string' && previousItem.val === upToSpec.val.toString(); | ||
if (typePrev === 'number') | ||
return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString(); | ||
} | ||
else if (upToSpec.path) { | ||
return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec ); | ||
} | ||
else if (upToSpec.sym) { | ||
return previousItem.sym && previousItem.sym.id === upToSpec.sym.id; | ||
} | ||
else if (upToSpec.struct && previousItem.struct) { | ||
return Object.entries( upToSpec.struct ) | ||
.every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) ); | ||
} | ||
return false; | ||
else if (literal === 'struct') { | ||
return Object.values( upToSpec.struct ).every( v => checkUpToSpec( v, art, annoName ) ); | ||
} | ||
function normalizeRef( node ) { // see to-csn.js | ||
const ref = pathName( node.path ); | ||
return node.variant ? `${ ref }#${ node.variant.id }` : ref; | ||
else if (literal !== 'array' && literal !== 'boolean' && literal !== 'null') { | ||
return true; | ||
} | ||
error( null, [ upToSpec.location, art ], | ||
{ anno: annoName, code: '... up to', '#': literal }, | ||
{ | ||
std: 'Unexpected $(CODE) value type in the assignment of $(ANNO)', | ||
array: 'Unexpected array as $(CODE) value in the assignment of $(ANNO)', | ||
// eslint-disable-next-line max-len | ||
struct: 'Unexpected structure as $(CODE) structure property value in the assignment of $(ANNO)', | ||
boolean: 'Unexpected boolean as $(CODE) value in the assignment of $(ANNO)', | ||
null: 'Unexpected null as $(CODE) value in the assignment of $(ANNO)', | ||
} ); | ||
return false; | ||
} | ||
function removeEllipsis(a, pos = findEllipsis( a )) { | ||
let count = 0; | ||
while (a.literal === 'array' && pos > -1) { | ||
count++; | ||
a.val.splice(pos, 1); | ||
pos = findEllipsis( a ); | ||
} | ||
return count; | ||
function equalUpTo( previousItem, upToSpec ) { | ||
if (!previousItem) | ||
return false; | ||
if ('val' in upToSpec) { | ||
if (previousItem.val === upToSpec.val) // enum, struct and ref have no val | ||
return true; | ||
const typeUpTo = typeof upToSpec.val; | ||
const typePrev = typeof previousItem.val; | ||
if (typeUpTo === 'number') | ||
return typePrev === 'string' && previousItem.val === upToSpec.val.toString(); | ||
if (typePrev === 'number') | ||
return typeUpTo === 'string' && upToSpec.val === previousItem.val.toString(); | ||
} | ||
function findEllipsis(a) { | ||
return (a.literal === 'array' && a.val) | ||
? a.val.findIndex(v => v.literal === 'token' && v.val === '...') : -1; | ||
else if (upToSpec.path) { | ||
return previousItem.path && normalizeRef( previousItem ) === normalizeRef( upToSpec ); | ||
} | ||
function findLayerCandidate() { | ||
// collect assignments of upper layers (are in no _layerExtends) | ||
const exts = Object.keys( layerAnnos ).map( layerExtends ); | ||
const allExtends = Object.assign( Object.create(null), ...exts ); | ||
const collected = []; | ||
for (const name in layerAnnos) { | ||
if (!(name in allExtends)) | ||
collected.push( prioritizedAnnos( layerAnnos[name].annos ) ); | ||
} | ||
// inspect collected assignments - choose the one or signal error | ||
const justOnePerLayer = collected.every( annos => annos.length === 1); | ||
if (!justOnePerLayer || collected.length > 1) { | ||
for (const annos of collected) { | ||
for (const a of annos ) { | ||
// Only the message ID is different. | ||
if (justOnePerLayer) { | ||
message( 'anno-duplicate-unrelated-layer', | ||
[ a.name.location, art ], { anno: annoName }, | ||
'Duplicate assignment with $(ANNO)' ); | ||
} | ||
else { | ||
message( 'anno-duplicate', [ a.name.location, art ], { anno: annoName } ); | ||
} | ||
} | ||
} | ||
} | ||
return collected[0][0]; // just choose any one with error | ||
else if (upToSpec.sym) { | ||
return previousItem.sym && previousItem.sym.id === upToSpec.sym.id; | ||
} | ||
function layerExtends( name ) { | ||
const { layer } = layerAnnos[name]; | ||
return layer && layer._layerExtends; | ||
else if (upToSpec.struct && previousItem.struct) { | ||
return Object.entries( upToSpec.struct ) | ||
.every( ([ n, v ]) => equalUpTo( previousItem.struct[n], v ) ); | ||
} | ||
return false; | ||
} | ||
function prioritizedAnnos( annos ) { | ||
let prio = 0; | ||
let r = []; | ||
for (const a of annos) { | ||
const p = annotationPriorities[a.$priority] || annotationPriorities.define; | ||
if (p === prio) { | ||
r.push(a); | ||
} | ||
else if (p > prio) { | ||
r = [ a ]; | ||
prio = p; | ||
} | ||
} | ||
return r; | ||
function normalizeRef( node ) { // see to-csn.js | ||
const ref = pathName( node.path ); | ||
return node.variant ? `${ ref }#${ node.variant.id }` : ref; | ||
} | ||
@@ -959,0 +934,0 @@ |
@@ -14,2 +14,3 @@ // Compiler functions and utilities shared across all phases | ||
pathName, | ||
annotationHasEllipsis, | ||
} = require('./utils'); | ||
@@ -516,3 +517,3 @@ | ||
if (typeArtifact.builtin) | ||
warning( 'type-ignoring-argument', loc, { art: typeArtifact } ); | ||
message( 'type-ignoring-argument', loc, { art: typeArtifact } ); | ||
else | ||
@@ -678,2 +679,5 @@ error( 'type-unexpected-argument', loc, { '#': 'std', art: typeArtifact }); | ||
return false; | ||
if (art.$requireElementAccess && path.length === 1) | ||
// Path with only one item, but we expect an element, e.g. `$at.from`. | ||
signalMissingElementAccess(art, [ item.location, user ]); | ||
continue; | ||
@@ -815,6 +819,25 @@ } | ||
/** | ||
* Emit a 'ref-expected-element' error for magic variable references | ||
* that require element accesses but don't do. | ||
* For example: `$at`, but `$at.from` or `$at.to` is required. | ||
* | ||
* @param {object} art | ||
* @param {any} location | ||
*/ | ||
function signalMissingElementAccess(art, location) { | ||
const err = message( 'ref-expected-element', location, | ||
{ '#': 'magicVar', id: art.name.id } ); | ||
// Mapping for better valid names: from -> $at.from | ||
const valid = Object.keys(art.elements || {}).reduce((prev, curr) => { | ||
prev[`${ art.name.id }.${ curr }`] = true; | ||
return prev; | ||
}, Object.create(null)); | ||
attachAndEmitValidNames(err, valid); | ||
} | ||
/** | ||
* Attaches a dictionary of valid names to the given compiler message. | ||
* In test mode, an info message is emitted with a list of valid names. | ||
* | ||
* @param {CSN.Message} msg CDS Compiler message | ||
* @param {CompileMessage} msg CDS Compiler message | ||
* @param {...object} validDicts One ore more artifact dictionaries such as in `_block`. | ||
@@ -845,10 +868,6 @@ */ | ||
// Resolve all annotation assignments for the node `art`. Set `art.@` to all | ||
// flattened assignments. This function might issue error message for | ||
// duplicate assignments. | ||
// TODOs: | ||
// * do something for extensions by CSN or Properties parsers | ||
// * make sure that we do not issue repeated warnings due to flattening if an | ||
// annotation definition is missing | ||
function defineAnnotations( construct, art, block, priority = 'define' ) { | ||
// Set _block links for annotations (necessary for layering). | ||
// Issue messages for annotations on namespaces and builtins (TODO: really here?) | ||
// Also copy annotations from `construct` to `art` (TODO: separate that functionality). | ||
function defineAnnotations( construct, art, block, priority = false ) { | ||
if (!options.parseCdl && construct.kind === 'annotate') { | ||
@@ -871,73 +890,25 @@ // Namespaces cannot be annotated in CSN but because they exist as XSN artifacts | ||
} | ||
// TODO: block should be construct._block | ||
if (construct.$annotations && construct.$annotations.doc ) | ||
art.doc = construct.$annotations.doc; // e.g. through `annotate` statement in CDL | ||
else if (construct.doc) | ||
if (construct.doc) | ||
art.doc = construct.doc; // e.g. through `extensions` array in CSN | ||
if (!construct.$annotations) { | ||
if (!block || block.$frontend !== 'json') | ||
return; // namespace, or in CDL source without @annos: | ||
// CSN input: set _block and $priority, shallow-copy from extension | ||
for (const annoProp in construct) { | ||
if (annoProp.charAt(0) === '@') { | ||
let annos = construct[annoProp]; | ||
if (!(Array.isArray(annos))) | ||
annos = [ annos ]; | ||
for (const a of annos) { | ||
setLink( a, '_block', block ); | ||
a.$priority = priority; | ||
if (construct !== art) | ||
addAnnotation( art, annoProp, a ); | ||
// set _block (for layering) and $priority, shallow-copy from extension | ||
// TODO: think of removing $priority, then | ||
// no _block: define, _block: annotate/extend/edmx | ||
// would fit with extending defs with props like length | ||
for (const annoProp in construct) { | ||
if (annoProp.charAt(0) === '@') { | ||
let annos = construct[annoProp]; | ||
if (!(Array.isArray(annos))) | ||
annos = [ annos ]; | ||
for (const a of annos) { | ||
setLink( a, '_block', block ); | ||
a.$priority = priority; // is now: undefined (auto-set) | false | 'annotate' | 'extend' | ||
if (construct !== art) | ||
addAnnotation( art, annoProp, a ); | ||
if (!priority && annotationHasEllipsis( a )) { | ||
error( 'anno-unexpected-ellipsis', | ||
[ a.name.location, art ], { code: '...' } ); | ||
} | ||
} | ||
} | ||
return; | ||
} | ||
for (const anno of construct.$annotations) { | ||
const ref = anno.name; | ||
const name = resolveUncheckedPath( ref, 'annotation', { _block: block } ); | ||
const annoProp = (anno.name.variant) | ||
? `@${ name }#${ anno.name.variant.id }` | ||
: `@${ name }`; | ||
flatten( ref.path, annoProp, anno.value || {}, anno.name.variant, anno.name.location ); | ||
} | ||
return; | ||
function flatten( path, annoProp, value, iHaveVariant, location ) { | ||
// Be robust if struct value has duplicate element names | ||
if (Array.isArray(value)) // TODO: do that differently in CDL parser | ||
return; // discard duplicates in flattened form | ||
if (value.literal === 'struct') { | ||
for (const item of value._struct || []) { | ||
let prop = pathName(item.name.path); | ||
if (item.name.variant) { | ||
if (iHaveVariant) { | ||
error( 'anno-duplicate-variant', [ item.name.variant.location, construct ], | ||
{}, // TODO: params | ||
'Annotation variant has been already provided' ); | ||
} | ||
prop = `${ prop }#${ item.name.variant.id }`; // TODO: check for double variants | ||
} | ||
flatten( [ ...path, ...item.name.path ], `${ annoProp }.${ prop }`, item, iHaveVariant || item.name.variant); | ||
} | ||
for (const prop in value.struct) { | ||
const item = value.struct[prop]; | ||
flatten( [ ...path, item.name ], `${ annoProp }.${ prop }`, item, iHaveVariant ); | ||
} | ||
return; | ||
} | ||
const anno = Object.assign( {}, value ); // shallow copy | ||
anno.name = { | ||
path, | ||
location: location || | ||
value.name && value.name.location || | ||
value.path && value.path.location, | ||
}; | ||
setLink( anno, '_block', block ); | ||
// TODO: _parent, _main is set later (if we have ElementRef), or do we | ||
// set _artifact? | ||
anno.$priority = priority; | ||
addAnnotation( art, annoProp, anno ); | ||
} | ||
} | ||
@@ -944,0 +915,0 @@ } |
@@ -42,3 +42,3 @@ // Tweak associations: rewrite keys and on conditions | ||
// behavior depending on option `deprecated`: | ||
const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' ); | ||
const enableExpandElements = !isDeprecatedEnabled( options, '_noElementsExpansion' ); | ||
// TODO: we should get rid of noElementsExpansion soon; both | ||
@@ -99,11 +99,10 @@ // beta.nestedProjections and beta.universalCsn do not work with it. | ||
if (!elem.$inferred) { // && !elem.target.$inferred | ||
// TODO: spec meeting 2021-01-22: no warning | ||
warning( 'assoc-target-not-in-service', [ elem.target.location, elem ], | ||
{ target, '#': (elem._main.query ? 'select' : 'define') }, { | ||
std: 'Target $(TARGET) of association is outside any service', // not used | ||
// eslint-disable-next-line max-len | ||
define: 'Target $(TARGET) of explicitly defined association is outside any service', | ||
// eslint-disable-next-line max-len | ||
select: 'Target $(TARGET) of explicitly selected association is outside any service', | ||
} ); | ||
info( 'assoc-target-not-in-service', [ elem.target.location, elem ], | ||
{ target, '#': (elem._main.query ? 'select' : 'define') }, { | ||
std: 'Target $(TARGET) of association is outside any service', // not used | ||
// eslint-disable-next-line max-len | ||
define: 'Target $(TARGET) of explicitly defined association is outside any service', | ||
// eslint-disable-next-line max-len | ||
select: 'Target $(TARGET) of explicitly selected association is outside any service', | ||
} ); | ||
} | ||
@@ -110,0 +109,0 @@ else { |
@@ -34,2 +34,6 @@ // Simple compiler utility functions | ||
} | ||
function annotationHasEllipsis( anno ) { | ||
const { val } = anno || {}; | ||
return Array.isArray( val ) && val.some( v => v.literal === 'token' && v.val === '...' ); | ||
} | ||
@@ -385,2 +389,3 @@ /** | ||
annotationIsFalse, | ||
annotationHasEllipsis, | ||
annotateWith, | ||
@@ -387,0 +392,0 @@ setLink, |
@@ -398,3 +398,3 @@ 'use strict'; | ||
const EntitySetName = edmUtils.getBaseName(entityCsn.$entitySetName || entityCsn.name); | ||
const isSingleton = edmUtils.isSingleton(entityCsn) && options.isV4(); | ||
const [ properties, hasStream ] = createProperties(entityCsn); | ||
@@ -406,3 +406,3 @@ | ||
warning(null, loc, { type }, 'EDM EntityType $(TYPE) has no properties'); | ||
else if(entityCsn.$edmKeyPaths.length === 0) | ||
else if(entityCsn.$edmKeyPaths.length === 0 && !isSingleton) | ||
message('odata-spec-violation-no-key', loc); | ||
@@ -693,5 +693,3 @@ | ||
if(!elementCsn._ignore) { | ||
if(edmUtils.isAssociationOrComposition(elementCsn)) | ||
{ | ||
if(edmUtils.isAssociationOrComposition(elementCsn)) { | ||
// Foreign keys are part of the generic elementCsn.elements property creation | ||
@@ -704,13 +702,13 @@ | ||
// (undefined !== false) still evaluates to true | ||
if (!elementCsn._target.abstract && elementCsn['@odata.navigable'] !== false) | ||
if (!elementCsn._target.abstract && elementCsn['@odata.navigable'] !== false) | ||
{ | ||
const navProp = new Edm.NavigationProperty(v, { | ||
Name: elementName, | ||
Type: elementCsn._target.name | ||
}, elementCsn); | ||
props.push(navProp); | ||
const navProp = new Edm.NavigationProperty(v, { | ||
Name: elementName, | ||
Type: elementCsn._target.name | ||
}, elementCsn); | ||
props.push(navProp); | ||
// save the navProp in the global array for late constraint building | ||
navigationProperties.push(navProp); | ||
} | ||
navigationProperties.push(navProp); | ||
} | ||
} | ||
// render ordinary property if element is NOT ... | ||
@@ -720,17 +718,16 @@ // 1) ... annotated @cds.api.ignore | ||
else if(isEdmPropertyRendered(elementCsn, options)) | ||
else if(isEdmPropertyRendered(elementCsn, options)) | ||
{ | ||
// CDXCORE-CDXCORE-173 | ||
// V2: filter @Core.MediaType | ||
if ( options.isV2() && elementCsn['@Core.MediaType']) { | ||
hasStream = elementCsn['@Core.MediaType']; | ||
delete elementCsn['@Core.MediaType']; | ||
if ( options.isV2() && elementCsn['@Core.MediaType']) { | ||
hasStream = elementCsn['@Core.MediaType']; | ||
delete elementCsn['@Core.MediaType']; | ||
// CDXCORE-CDXCORE-177: | ||
// V2: don't render element but add attribute 'm:HasStream="true' to EntityType | ||
// V4: render property type 'Edm.Stream' | ||
streamProps.push(elementName); | ||
streamProps.push(elementName); | ||
} else { | ||
props.push(new Edm.Property(v, { Name: elementName }, elementCsn)); | ||
} | ||
} else { | ||
props.push(new Edm.Property(v, { Name: elementName }, elementCsn)); | ||
} | ||
@@ -737,0 +734,0 @@ } |
@@ -11,28 +11,14 @@ 'use strict'; | ||
{ | ||
// csn2edm expects "version" to be a top-level property of options | ||
// csn2edm expects "odataVersion" to be a top-level property of options | ||
// set to 'v4' as default, override with value from incoming options | ||
// (here version comes inside "toOdata") | ||
const options = Object.assign({ version: 'v4'}, _options); | ||
if (options.toOdata) { | ||
if(options.toOdata.version) | ||
options.version = options.toOdata.version; | ||
if(options.toOdata.odataFormat) | ||
options.odataFormat = options.toOdata.odataFormat; | ||
if(options.toOdata.odataContainment) | ||
options.odataContainment = options.toOdata.odataContainment; | ||
if(options.toOdata.odataForeignKeys) | ||
options.odataForeignKeys = options.toOdata.odataForeignKeys; | ||
if(options.toOdata.odataV2PartialConstr) | ||
options.odataV2PartialConstr = options.toOdata.odataV2PartialConstr; | ||
// global flag that indicates whether or not FKs shall be rendered in general | ||
// V2/V4 flat: yes | ||
// V4/struct: depending on odataForeignKeys | ||
options.renderForeignKeys = | ||
options.version === 'v4' ? options.odataFormat === 'structured' && !!options.odataForeignKeys : true; | ||
const options = Object.assign({ odataVersion: 'v4'}, _options); | ||
// global flag that indicates whether or not FKs shall be rendered in general | ||
// V2/V4 flat: yes | ||
// V4/struct: depending on odataForeignKeys | ||
options.renderForeignKeys = | ||
options.odataVersion === 'v4' ? options.odataFormat === 'structured' && !!options.odataForeignKeys : true; | ||
} | ||
const v2 = options.odataVersion.match(/v2/i) !== null; | ||
const v4 = options.odataVersion.match(/v4/i) !== null; | ||
const v2 = options.version.match(/v2/i) !== null; | ||
const v4 = options.version.match(/v4/i) !== null; | ||
options.v = [v2, v4]; | ||
@@ -39,0 +25,0 @@ options.isStructFormat = options.odataFormat && options.odataFormat === 'structured'; |
@@ -138,3 +138,3 @@ // CSN frontend - transform CSN into XSN | ||
type: condition, | ||
msgId: 'syntax-csn-expected-term', | ||
msgId: 'syntax-expected-term', | ||
// TODO: also specify requires here, and adapt onlyWith() | ||
@@ -149,7 +149,7 @@ optional: exprProperties, | ||
type: natnumOrStar, | ||
msgId: 'syntax-csn-expected-cardinality', | ||
msgId: 'syntax-expected-cardinality', | ||
}, | ||
columns: { | ||
arrayOf: selectItem, | ||
msgId: 'syntax-csn-expected-column', | ||
msgId: 'syntax-expected-column', | ||
defaultKind: '$column', | ||
@@ -295,3 +295,3 @@ validKinds: [], // pseudo kind '$column' | ||
type: artifactRef, | ||
msgId: 'syntax-csn-expected-reference', | ||
msgId: 'syntax-expected-reference', | ||
optional: [ 'ref', 'global' ], | ||
@@ -372,3 +372,3 @@ inKind: [ 'element', 'type', 'param', 'mixin', 'event', 'annotation' ], | ||
type: renameTo( 'path', arrayOf( refItem ) ), | ||
msgId: 'syntax-csn-expected-reference', | ||
msgId: 'syntax-expected-reference', | ||
minLength: 1, | ||
@@ -589,3 +589,3 @@ requires: 'id', | ||
masked: { | ||
type: boolOrNull, | ||
type: masked, | ||
inKind: [ 'element' ], | ||
@@ -787,3 +787,3 @@ }, | ||
if (minLength > val.length) { | ||
message( 'syntax-csn-expected-length', location(true), | ||
message( 'syntax-expected-length', location(true), | ||
{ prop: spec.prop, n: minLength, '#': minLength === 1 ? 'one' : 'std' }); | ||
@@ -858,3 +858,3 @@ } | ||
if (!relevantProps) { | ||
error( 'syntax-csn-required-subproperty', location(true), | ||
error( 'syntax-required-subproperty', location(true), | ||
{ | ||
@@ -885,3 +885,3 @@ prop: spec.msgProp, | ||
if (!csnVersionZero) { | ||
warning( 'syntax-csn-zero-delete', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-zero-delete', location(true), { prop: spec.msgProp }, | ||
'Delete/inline CSN v0.1.0 property $(PROP)' ); | ||
@@ -971,3 +971,3 @@ } | ||
if (!dict || typeof dict !== 'object' || Array.isArray( dict )) { | ||
error( 'syntax-csn-expected-object', location(true), | ||
error( 'syntax-expected-object', location(true), | ||
{ prop: spec.prop }); // spec.prop, not spec.msgProp! | ||
@@ -984,3 +984,3 @@ return ignore( dict ); | ||
if (!name) { | ||
warning( 'syntax-csn-empty-name', location(true), | ||
warning( 'syntax-empty-name', location(true), | ||
{ prop: spec.prop }, // TODO: Error | ||
@@ -1062,7 +1062,7 @@ 'Property names in dictionary $(PROP) must not be empty' ); | ||
if (val === 'view' && xsn.kind === 'entity') { | ||
warning( 'syntax-csn-zero-value', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-zero-value', location(true), { prop: spec.msgProp }, | ||
'Replace CSN v0.1.0 value in $(PROP) by something specified' ); | ||
} | ||
else if ((val === 'entity' || val === 'type') && xsn.kind === 'aspect') { | ||
info( 'syntax-csn-aspect', location(true), { kind: 'aspect', '#': val }, | ||
info( 'syntax-aspect', location(true), { kind: 'aspect', '#': val }, | ||
{ | ||
@@ -1075,3 +1075,3 @@ std: 'Use the dedicated kind $(KIND) for aspect definitions', | ||
else { | ||
error( 'syntax-csn-expected-valid', location(true), { prop: spec.msgProp }, | ||
error( 'syntax-expected-valid', location(true), { prop: spec.msgProp }, | ||
'Expected valid string for property $(PROP)' ); | ||
@@ -1118,3 +1118,3 @@ } | ||
if (!path.every( id => id)) { | ||
warning( 'syntax-csn-expected-name', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-expected-name', location(true), { prop: spec.msgProp }, | ||
'Expected correct name for property $(PROP)' ); | ||
@@ -1130,3 +1130,3 @@ } | ||
return { val, location: location() }; | ||
warning( 'syntax-csn-expected-boolean', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-expected-boolean', location(true), { prop: spec.msgProp }, | ||
'Expected boolean or null for property $(PROP)' ); | ||
@@ -1141,3 +1141,3 @@ ignore( val ); | ||
return val; | ||
error( 'syntax-csn-expected-string', location(true), { prop: spec.msgProp }, | ||
error( 'syntax-expected-string', location(true), { prop: spec.msgProp }, | ||
'Expected non-empty string for property $(PROP)' ); | ||
@@ -1151,3 +1151,3 @@ return ignore( val ); | ||
return { val, literal: 'string', location: location() }; | ||
error( 'syntax-csn-expected-string', location(true), { prop: spec.msgProp }, | ||
error( 'syntax-expected-string', location(true), { prop: spec.msgProp }, | ||
'Expected non-empty string for property $(PROP)' ); | ||
@@ -1174,3 +1174,3 @@ return ignore( val ); | ||
return { val, literal: 'number', location: location() }; | ||
error( spec.msgId || 'syntax-csn-expected-natnum', location(true), | ||
error( spec.msgId || 'syntax-expected-natnum', location(true), | ||
{ prop: spec.msgProp } ); | ||
@@ -1210,6 +1210,4 @@ return ignore( val ); | ||
if (arrayLevelCount > 0) { // TODO: also inside structure (possible in CSN!) | ||
if (val.some( isEllipsis )) { | ||
error( 'syntax-csn-unexpected-ellipsis', location(true), { code: '...' }, | ||
'Unexpected $(CODE) in nested array' ); | ||
} | ||
if (val.some( isEllipsis )) | ||
error( 'syntax-unexpected-ellipsis', location(true), { '#': 'nested-array', code: '...' } ); | ||
} | ||
@@ -1223,3 +1221,3 @@ else { | ||
// error position at the beginning of the array, but that is fine | ||
error( 'syntax-csn-duplicate-ellipsis', location(true), { code: '...' }, | ||
error( 'syntax-duplicate-ellipsis', location(true), { code: '...' }, | ||
'Expected no more than one $(CODE)' ); | ||
@@ -1238,3 +1236,3 @@ break; | ||
if (seenEllipsis === 'upTo') { | ||
error( 'syntax-csn-expecting-ellipsis', location(true), // at closing bracket | ||
error( 'syntax-expecting-ellipsis', location(true), // at closing bracket | ||
{ code: '... up to', newcode: '...' }, | ||
@@ -1286,7 +1284,10 @@ // TODO: should we be more CSN specific in the message? | ||
function annotation( val, spec, xsn, csn, name ) { | ||
const variantIndex = name.indexOf('#') + 1 || name.length; | ||
const n = refSplit( name.substring( (xsn ? 1 : 0), variantIndex ), spec.msgProp ); | ||
const absolute = (xsn ? name.substring(1) : name); | ||
// TODO: really care about variant (qualifier parts)? | ||
const variantIndex = absolute.indexOf('#') + 1 || absolute.length; | ||
const n = refSplit( absolute.substring( 0, variantIndex ), spec.msgProp ); | ||
if (!n) | ||
return undefined; | ||
if (variantIndex < name.length) | ||
n.absolute = absolute; | ||
if (variantIndex < absolute.length) | ||
n.variant = { id: name.substring( variantIndex ), location: location() }; | ||
@@ -1306,3 +1307,3 @@ const r = annoValue( val, spec ); | ||
} | ||
error( 'syntax-csn-expected-scalar', location(true), { prop: spec.msgProp }, | ||
error( 'syntax-expected-scalar', location(true), { prop: spec.msgProp }, | ||
'Only scalar values are supported for property $(PROP)' ); | ||
@@ -1319,3 +1320,3 @@ return ignore( val ); | ||
return val; | ||
error( 'syntax-csn-expected-valid', location(true), { prop: spec.msgProp }, | ||
error( 'syntax-expected-valid', location(true), { prop: spec.msgProp }, | ||
'Expected valid string for property $(PROP)' ); | ||
@@ -1335,3 +1336,3 @@ return ignore( val ); | ||
if (!exprs.length) { | ||
message( 'syntax-csn-expected-length', location(true), | ||
message( 'syntax-expected-length', location(true), | ||
{ prop: 'xpr', otherprop: 'func', '#': 'suffix' }); | ||
@@ -1369,3 +1370,3 @@ } | ||
else if (!exprs || typeof exprs !== 'object') { | ||
error( 'syntax-csn-expected-args', location(true), | ||
error( 'syntax-expected-args', location(true), | ||
{ prop: spec.prop }, // spec.prop, not spec.msgProp! | ||
@@ -1433,3 +1434,3 @@ 'Expected array or object for property $(PROP)' ); | ||
// TODO: also "sign" xsn.value created by inValue to complain about both 'value' and 'ref' etc | ||
warning( 'syntax-csn-unexpected-property', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-unexpected-property', location(true), { prop: spec.msgProp }, | ||
'Unexpected CSN property $(PROP)' ); | ||
@@ -1439,3 +1440,3 @@ return undefined; | ||
if (!csnVersionZero) { | ||
warning( 'syntax-csn-zero-delete', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-zero-delete', location(true), { prop: spec.msgProp }, | ||
'Delete/inline CSN v0.1.0 property $(PROP)' ); | ||
@@ -1487,2 +1488,8 @@ } | ||
function masked( val, spec ) { | ||
message('syntax-invalid-masked', location(), { keyword: 'masked' }, | ||
'Keyword $(KEYWORD) not supported'); | ||
return boolOrNull( val, spec ); | ||
} | ||
function duplicateExcluding( name, loc ) { | ||
@@ -1508,3 +1515,3 @@ error( 'duplicate-excluding', loc, { name, keyword: 'excluding' }, | ||
if (Array.isArray( val ) && val.length > 1 && !csn.op) { | ||
warning( 'syntax-csn-expected-property', location(true), | ||
warning( 'syntax-expected-property', location(true), | ||
{ prop: 'args', otherprop: 'op' }, | ||
@@ -1528,3 +1535,3 @@ 'CSN property $(PROP) expects property $(OTHERPROP) to be specified' ); | ||
return { val: keyVal, literal: 'string', location: location() }; | ||
error( 'syntax-csn-expected-translation', location(true), | ||
error( 'syntax-expected-translation', location(true), | ||
{ prop: textKey, otherprop: spec.prop }, | ||
@@ -1543,3 +1550,3 @@ 'Expected string for text key $(PROP) of language $(OTHERPROP)' ); | ||
// TODO v2: Warning only with --sloppy | ||
warning( 'syntax-csn-unknown-property', location(true), { prop }, | ||
warning( 'syntax-unknown-property', location(true), { prop }, | ||
'Unknown CSN property $(PROP)' ); | ||
@@ -1555,3 +1562,3 @@ } | ||
if (s.vZeroIgnore && s.vZeroIgnore === csn[prop]) { // for "op": "call" | ||
warning( 'syntax-csn-zero-delete', location(true), { prop }, | ||
warning( 'syntax-zero-delete', location(true), { prop }, | ||
'Delete/inline CSN v0.1.0 property $(PROP)' ); | ||
@@ -1575,3 +1582,3 @@ return { type: ignore }; | ||
: (parentSpec.msgProp ? 'std' : 'top'); | ||
message( 'syntax-csn-unexpected-property', location(true), | ||
message( 'syntax-unexpected-property', location(true), | ||
{ | ||
@@ -1634,3 +1641,3 @@ prop, otherprop: parentSpec.msgProp, kind, '#': variant, | ||
if (prop) { | ||
error( 'syntax-csn-dependent-property', location(true), | ||
error( 'syntax-dependent-property', location(true), | ||
{ prop, otherprop: need }, | ||
@@ -1641,3 +1648,3 @@ 'CSN property $(PROP) can only be used in combination with $(OTHERPROP)'); | ||
else if (!xor['no:req']) { | ||
error( 'syntax-csn-required-property', location(true), | ||
error( 'syntax-required-property', location(true), | ||
{ prop: need, otherprop: spec.msgProp, '#': spec.prop }, | ||
@@ -1664,3 +1671,3 @@ { // TODO $(PARENT), TODO: do not use prop===0 hack | ||
return true; // hack for window function: both func and xpr is allowed | ||
error( 'syntax-csn-excluded-property', location(true), | ||
error( 'syntax-excluded-property', location(true), | ||
{ prop, otherprop: xor[group] }, | ||
@@ -1680,3 +1687,3 @@ 'CSN property $(PROP) can only be used alternatively to $(OTHERPROP)'); | ||
return; | ||
warning( 'syntax-csn-zero-prop', location(true), { prop, otherprop }, | ||
warning( 'syntax-zero-prop', location(true), { prop, otherprop }, | ||
'Replace CSN v0.1.0 property $(OTHERPROP) by $(PROP)' ); | ||
@@ -1690,3 +1697,3 @@ } | ||
return array; | ||
error( 'syntax-csn-expected-array', location(true), { prop: spec.prop }, | ||
error( 'syntax-expected-array', location(true), { prop: spec.prop }, | ||
'Expected array for property $(PROP)' ); | ||
@@ -1699,3 +1706,3 @@ return ignore( array ); | ||
return obj; | ||
error( spec.msgId || 'syntax-csn-expected-object', location(true), | ||
error( spec.msgId || 'syntax-expected-object', location(true), | ||
{ prop: spec.msgProp }); | ||
@@ -1708,3 +1715,3 @@ return ignore( obj ); | ||
if (!path.every( id => id)) { | ||
warning( 'syntax-csn-expected-name', location(true), { prop }, | ||
warning( 'syntax-expected-name', location(true), { prop }, | ||
'Expected correct name for property $(PROP)' ); | ||
@@ -1717,3 +1724,3 @@ } | ||
if (!csnVersionZero && spec.vZeroFor == null) { // but 0 does not match! | ||
warning( 'syntax-csn-zero-value', location(true), { prop: spec.msgProp }, | ||
warning( 'syntax-zero-value', location(true), { prop: spec.msgProp }, | ||
'Replace CSN v0.1.0 value in $(PROP) by something specified' ); | ||
@@ -1748,3 +1755,3 @@ } | ||
dollarLocations.push( null ); // must match with popLocation() | ||
error( 'syntax-csn-expected-object', location(true), { prop: '$location' } ); | ||
error( 'syntax-expected-object', location(true), { prop: '$location' } ); | ||
} | ||
@@ -1862,3 +1869,3 @@ // hidden feature: string $location | ||
}; | ||
messageFunctions.error( 'syntax-csn-illegal-json', loc, { msg }, 'Illegal JSON: $(MSG)' ); | ||
messageFunctions.error( 'syntax-illegal-json', loc, { msg }, 'Illegal JSON: $(MSG)' ); | ||
return xsn; | ||
@@ -1865,0 +1872,0 @@ } |
@@ -225,3 +225,3 @@ // Transform XSN (augmented CSN) into CSN | ||
frameBetween: exprs => [ 'between', ...exprs[0], 'and', ...exprs[1] ], | ||
// xpr: (exprs) => [].concat( ...exprs ), see below - handled extra | ||
ixpr: exprs => [].concat( ...exprs ), // xpr extra, due to extra parentheses | ||
}; | ||
@@ -729,3 +729,4 @@ | ||
// and @odata.containment.ignore | ||
if (val.$priority && (val.$priority !== 'define') === annotated) { | ||
// TODO: use $inferred instead special $priority value | ||
if (val.$priority !== undefined && (!!val.$priority) === annotated) { | ||
// transformer (= value) takes care to exclude $inferred annotation assignments | ||
@@ -1593,6 +1594,7 @@ const sub = transformer( val ); | ||
gensrcFlavor = options.parseCdl || options.csnFlavor === 'gensrc' || | ||
options.toCsn && options.toCsn.flavor === 'gensrc'; | ||
universalCsn = (options.csnFlavor === 'universal' || | ||
options.toCsn && options.toCsn.flavor === 'universal' ) && | ||
isBetaEnabled( options, 'enableUniversalCsn' ) && !options.parseCdl; | ||
( options.toCsn && options.toCsn.flavor === 'gensrc'); | ||
universalCsn = ( options.csnFlavor === 'universal' || | ||
( options.toCsn && options.toCsn.flavor === 'universal') ) && | ||
isBetaEnabled( options, 'enableUniversalCsn' ) && | ||
!options.parseCdl; | ||
strictMode = options.testMode; | ||
@@ -1605,4 +1607,4 @@ const proto = options.dictionaryPrototype; | ||
withLocations = options.withLocations; | ||
parensAsStrings = isDeprecatedEnabled( options, 'parensAsStrings' ); | ||
projectionAsQuery = isDeprecatedEnabled( options, 'projectionAsQuery' ); | ||
parensAsStrings = isDeprecatedEnabled( options, '_parensAsStrings' ); | ||
projectionAsQuery = isDeprecatedEnabled( options, '_projectionAsQuery' ); | ||
} | ||
@@ -1609,0 +1611,0 @@ |
@@ -16,4 +16,4 @@ // @ts-nocheck : Issues with Tokens on `this`, e.g. `this.DOT`. | ||
const Parser = require('../gen/languageParser').languageParser; | ||
const Lexer = require('../gen/languageLexer').languageLexer; | ||
const Parser = require('../gen/languageParser').default; | ||
const Lexer = require('../gen/languageLexer').default; | ||
@@ -20,0 +20,0 @@ // Error listener used for ANTLR4-generated parser |
'use strict'; | ||
const { splitLines } = require('../utils/file'); | ||
const { | ||
isWhitespaceOrNewLineOnly, | ||
isWhitespaceCharacterNoNewline, | ||
cdlNewLineRegEx, | ||
} = require('./textUtils'); | ||
@@ -21,24 +25,26 @@ /** | ||
let lines = splitLines(comment); | ||
let lines = comment.split(cdlNewLineRegEx); | ||
if (lines.length === 1) { | ||
// special case for one-liners | ||
// remove "/***/" and trim white space | ||
const content = lines[0].replace(/^\/[*]{2,}/, '').replace(/\*\/$/, '').trim(); | ||
return isWhiteSpaceOnly(content) ? null : content; | ||
// Special case for one-liners. | ||
// Remove "/***/" and trim white space and asterisks. | ||
const content = lines[0] | ||
.replace(/^\/[*]{2,}/, '') | ||
.replace(/\*+\/$/, '') | ||
.trim(); | ||
return isWhitespaceOrNewLineOnly(content) ? null : content; | ||
} | ||
lines[0] = removeHeaderFence(lines[0]); | ||
// If the comment already has content on the first line, i.e. after `/**`, | ||
// its leading whitespace is ignored for whitespace trimming. | ||
const hasContentOnFirstLine = /\/\*+\s*\S/.test(lines[0]); | ||
// First line, i.e. header, is always trimmed from left. | ||
lines[0] = removeHeaderFence(lines[0]).trimStart(); | ||
lines[lines.length - 1] = removeFooterFence(lines[lines.length - 1]); | ||
if (isFencedComment(lines)) { | ||
lines = lines.map((line, index) => ((index === 0) ? line : removeFence(line))); | ||
} | ||
else if (lines.length === 2) { | ||
if (lines.length === 2) { | ||
// Comment that is essentially just a header + footer. | ||
// First line, i.e. header, is always trimmed from left. | ||
lines[0] = lines[0].trimStart(); | ||
// If the second line starts with an asterisk then remove it. | ||
// Otherwise trim all whitespace. | ||
// Otherwise, trim all left whitespace. | ||
if ((/^\s*[*]/.test(lines[1]))) | ||
@@ -49,8 +55,7 @@ lines[1] = removeFence(lines[1]); | ||
} | ||
else if (isFencedComment(lines)) { | ||
lines = lines.map((line, index) => ((index === 0) ? line : removeFence(line))); | ||
} | ||
else { | ||
const firstNonEmptyLine = lines.find((line, index) => index !== 0 && /[^\s]/.test(line)) || ''; | ||
// Tabs are regarded as one space. | ||
const spacesAtBeginning = firstNonEmptyLine.match(/^\s*/)[0].length; | ||
if (spacesAtBeginning > 0) | ||
lines = lines.map(line => removeWhitespace(line, spacesAtBeginning)); | ||
stripCommentIndentation(lines, hasContentOnFirstLine); | ||
} | ||
@@ -63,14 +68,43 @@ | ||
const content = lines.slice(startIndex, endIndex).join('\n'); | ||
return isWhiteSpaceOnly(content) ? null : content; | ||
return isWhitespaceOrNewLineOnly(content) ? null : content; | ||
} | ||
/** | ||
* Checks whether the given string is whitespace only, i.e. newline | ||
* spaces, tabs. | ||
* Strips and counts the indentation from the given comment string. | ||
* This function is similar to the one in multiLineStringParser.js, but does not | ||
* have special handling for the first and last line of the string. | ||
* | ||
* @param {string} content | ||
* @example | ||
* | hello | ||
* | world | ||
* | foo bar | ||
* becomes | ||
* | hello | ||
* | world | ||
* | foo bar | ||
* | ||
* @param {string[]} lines String split into lines. | ||
* @param {boolean} ignoreFirstLine Whether to ignore the first line for indentation counting. | ||
*/ | ||
function isWhiteSpaceOnly(content) { | ||
return /^\s*$/.test(content); | ||
function stripCommentIndentation(lines, ignoreFirstLine) { | ||
const n = lines.length; | ||
const minIndent = lines.reduce((min, line, index) => { | ||
// Blank lines are ignored. | ||
if (isWhitespaceOrNewLineOnly(line) || (index === 0 && ignoreFirstLine)) | ||
return min; | ||
let count = 0; | ||
const length = Math.min(min, line.length); | ||
while (count < length && isWhitespaceCharacterNoNewline(line[count])) { | ||
count++; | ||
} | ||
return Math.min(min, count); | ||
}, Number.MAX_SAFE_INTEGER); | ||
for (let i = (ignoreFirstLine ? 1 : 0); i < n; ++i) { | ||
// Note: Line may be empty and have fewer characters than `min`. | ||
// In that case, slice() returns an empty string. | ||
lines[i] = lines[i].slice(minIndent); | ||
} | ||
} | ||
@@ -92,13 +126,2 @@ | ||
/** | ||
* Remove the TODO | ||
* | ||
* @param {string} line | ||
* @param {number} spaces Number of whitespace to remove at the beginning of the line | ||
* @returns {string} line without fence | ||
*/ | ||
function removeWhitespace(line, spaces) { | ||
return line.replace(new RegExp(`^\\s{0,${ spaces }}`), ''); // Trailing spaces with '*'? => .replace(/\s+[*]$/, ''); | ||
} | ||
/** | ||
* Removes a header fence, i.e. '/**'. | ||
@@ -105,0 +128,0 @@ * May remove more than two asterisks e.g. '/*******' |
@@ -32,8 +32,8 @@ // Error strategy with special handling for (non-reserved) keywords | ||
const antlr4 = require('antlr4'); | ||
const IntervalSet = require('antlr4/IntervalSet'); | ||
const antlr4_error = require('antlr4/error/ErrorStrategy'); | ||
const antlr4_LL1Analyzer = require('antlr4/LL1Analyzer.js').LL1Analyzer; | ||
const predictionContext = require('antlr4/PredictionContext').predictionContextFromRuleContext; | ||
const { ATNState } = require('antlr4/atn/ATNState'); | ||
const { InputMismatchException } = antlr4.error; | ||
const antlr4_LL1Analyzer = require('antlr4/src/antlr4/LL1Analyzer'); | ||
const { DefaultErrorStrategy } = require('antlr4/src/antlr4/error/ErrorStrategy'); | ||
const { InputMismatchException } = require('antlr4/src/antlr4/error/Errors'); | ||
const { predictionContextFromRuleContext: predictionContext } = require('antlr4/src/antlr4/PredictionContext'); | ||
const { ATNState } = require('antlr4/src/antlr4/atn/ATNState'); | ||
const { IntervalSet, Interval } = require('antlr4/src/antlr4/IntervalSet'); | ||
@@ -69,26 +69,31 @@ const keywordRegexp = /^[a-zA-Z]+$/; // we don't have keywords with underscore | ||
// parser (prototype). | ||
function KeywordErrorStrategy( ...args ) { | ||
antlr4_error.DefaultErrorStrategy.call( this, ...args ); | ||
} | ||
const super1 = antlr4_error.DefaultErrorStrategy.prototype; | ||
class KeywordErrorStrategy extends DefaultErrorStrategy { | ||
constructor( ...args ) { | ||
super( ...args ); | ||
KeywordErrorStrategy.prototype = Object.assign( | ||
Object.create( super1 ), { | ||
sync, | ||
reportNoViableAlternative, | ||
reportInputMismatch, | ||
reportUnwantedToken, | ||
reportMissingToken, | ||
reportIgnoredWith, | ||
// getErrorRecoverySet, | ||
consumeUntil, | ||
recoverInline, | ||
getMissingSymbol, | ||
getExpectedTokensForMessage, | ||
getTokenDisplay, | ||
constructor: KeywordErrorStrategy, | ||
this._super = { | ||
consumeUntil: super.consumeUntil, | ||
recoverInline: super.recoverInline, | ||
getExpectedTokens: super.getExpectedTokens, | ||
}; | ||
} | ||
); | ||
} | ||
// Attemp to recover from problems in subrules, except if rule has defined a | ||
// TODO: Use actual methods | ||
Object.assign( KeywordErrorStrategy.prototype, { | ||
sync, | ||
reportNoViableAlternative, | ||
reportInputMismatch, | ||
reportUnwantedToken, | ||
reportMissingToken, | ||
reportIgnoredWith, | ||
// getErrorRecoverySet, | ||
consumeUntil, | ||
recoverInline, | ||
getMissingSymbol, | ||
getExpectedTokensForMessage, | ||
getTokenDisplay, | ||
}); | ||
// Attempt to recover from problems in subrules, except if rule has defined a | ||
// local variable `_sync` with value 'nop' | ||
@@ -171,3 +176,3 @@ function sync( recognizer ) { | ||
this.reportUnwantedToken(recognizer); | ||
const expecting = new IntervalSet.IntervalSet(); | ||
const expecting = new IntervalSet(); | ||
expecting.addSet(recognizer.getExpectedTokens()); | ||
@@ -276,6 +281,6 @@ const whatFollowsLoopIterationOrRule = expecting.addSet(this.getErrorRecoverySet(recognizer)); | ||
if (SEMI < 1 || RBRACE < 1) { | ||
super1.consumeUntil.call( this, recognizer, set ); | ||
this._super.consumeUntil.call( this, recognizer, set ); | ||
} | ||
else if (set.contains(SEMI)) { // do not check for RBRACE here! | ||
super1.consumeUntil.call( this, recognizer, set ); | ||
this._super.consumeUntil.call( this, recognizer, set ); | ||
// console.log('CONSUMED-ORIG:',s,this.getTokenDisplay( recognizer.getCurrentToken(), recognizer ),recognizer.getCurrentToken().line,intervalSetToArray( recognizer, set )); | ||
@@ -285,3 +290,3 @@ } | ||
// DO NOT modify input param `set`, as the set might be cached in the ATN | ||
const stop = new IntervalSet.IntervalSet(); | ||
const stop = new IntervalSet(); | ||
stop.addSet( set ); | ||
@@ -292,3 +297,3 @@ stop.removeOne( recognizer.constructor.Identifier ); | ||
stop.addOne( RBRACE ); | ||
super1.consumeUntil.call( this, recognizer, stop ); | ||
this._super.consumeUntil.call( this, recognizer, stop ); | ||
if (recognizer.getTokenStream().LA(1) === SEMI || | ||
@@ -319,7 +324,7 @@ recognizer.getTokenStream().LA(1) === RBRACE && !set.contains(RBRACE)) { | ||
if (!identType || !recognizer.isExpectedToken( identType )) | ||
return super1.recoverInline.call( this, recognizer ); | ||
return this._super.recoverInline.call( this, recognizer ); | ||
const token = recognizer.getCurrentToken(); | ||
if (!keywordRegexp.test( token.text )) | ||
return super1.recoverInline.call( this, recognizer ); | ||
return this._super.recoverInline.call( this, recognizer ); | ||
@@ -354,4 +359,8 @@ recognizer.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text }, | ||
// which are function name and argument position dependent: | ||
if (j === pc.GenericArgFull) | ||
names.push( ...recognizer.$genericKeywords.argFull ); | ||
if (j === pc.GenericExpr) | ||
names.push( ...recognizer.$genericKeywords.expr ); | ||
else if (j === pc.GenericSeparator) | ||
names.push( ...recognizer.$genericKeywords.separator ); | ||
else if (j === pc.GenericIntro) | ||
names.push( ...recognizer.$genericKeywords.introMsg ); | ||
// other expected tokens usually appear in messages, except the helper tokens | ||
@@ -373,2 +382,5 @@ // which are used to solve ambiguities via the parser method setLocalToken(): | ||
} | ||
else if (names.includes("'?'")) { | ||
names = names.filter( n => n !== "'?'" ); | ||
} | ||
names.sort( (a, b) => (tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1) ); | ||
@@ -433,6 +445,6 @@ return names; | ||
if (!identType || !beforeUnreserved || beforeUnreserved + 2 > identType) | ||
return intervalSetToArray( recognizer, super1.getExpectedTokens.call( this, recognizer ) ); | ||
return intervalSetToArray( recognizer, this._super.getExpectedTokens.call( this, recognizer ) ); | ||
const ll1 = new antlr4_LL1Analyzer(atn); | ||
const expected = new IntervalSet.IntervalSet(); | ||
const expected = new IntervalSet(); | ||
const orig_addInterval = expected.addInterval; | ||
@@ -498,3 +510,3 @@ const orig_addSet = expected.addSet; | ||
function addRange( interval, start, stop ) { | ||
orig_addInterval.call( interval, new IntervalSet.Interval( start, stop || start + 1 ) ); | ||
orig_addInterval.call( interval, new Interval( start, stop || start + 1 ) ); | ||
} | ||
@@ -501,0 +513,0 @@ } |
@@ -10,3 +10,3 @@ // Generic ANTLR parser class with AST-building functions | ||
const antlr4 = require('antlr4'); | ||
const { ATNState } = require('antlr4/atn/ATNState'); | ||
const { ATNState } = require('antlr4/src/antlr4/atn/ATNState'); | ||
const { dictAdd, dictAddArray } = require('../base/dictionaries'); | ||
@@ -36,82 +36,83 @@ const locUtils = require('../base/location'); | ||
// | ||
function GenericAntlrParser( ...args ) { | ||
// ANTLR restriction: we cannot add parameters to the constructor. | ||
antlr4.Parser.call( this, ...args ); | ||
this.buildParseTrees = false; | ||
class GenericAntlrParser extends antlr4.Parser { | ||
constructor( ...args ) { | ||
// ANTLR restriction: we cannot add parameters to the constructor. | ||
super( ...args ); | ||
this.buildParseTrees = false; | ||
// Common properties. | ||
// We set them here so that they are available in the prototype. | ||
// This improved performance by 25% for certain scenario tests. | ||
// Probably because there was no need to look up the prototype chain anymore. | ||
this.$adaptExpectedToken = null; | ||
this.$adaptExpectedExcludes = [ ]; | ||
this.$nextTokensToken = null; | ||
this.$nextTokensContext = null; | ||
// Common properties. | ||
// We set them here so that they are available in the prototype. | ||
// This improved performance by 25% for certain scenario tests. | ||
// Probably because there was no need to look up the prototype chain anymore. | ||
this.$adaptExpectedToken = null; | ||
this.$adaptExpectedExcludes = [ ]; | ||
this.$nextTokensToken = null; | ||
this.$nextTokensContext = null; | ||
this.prepareGenericKeywords(); | ||
this.options = {}; | ||
this.options = {}; | ||
return this; | ||
this.genericFunctionsStack = []; | ||
this.$genericKeywords = specialFunctions[''][1]; | ||
} | ||
} | ||
// When we define this class with the ES6 `class` syntax, we get | ||
// TypeError: Class constructors cannot be invoked without 'new' | ||
// Reason: the generated ANTLR constructor calls its super constructor via | ||
// old-style `<super>.call(this,...)`, not via `super(...)`. | ||
// TODO: Use actual methods. | ||
Object.assign(GenericAntlrParser.prototype, { | ||
message: function(...args) { return _message( this, 'message', ...args ); }, | ||
error: function(...args) { return _message( this, 'error', ...args ); }, | ||
warning: function(...args) { return _message( this, 'warning', ...args ); }, | ||
info: function(...args) { return _message( this, 'info', ...args ); }, | ||
attachLocation, | ||
assignAnnotation, | ||
startLocation, | ||
tokenLocation, | ||
valueWithTokenLocation, | ||
previousTokenAtLocation, | ||
combinedLocation, | ||
surroundByParens, | ||
unaryOpForParens, | ||
leftAssocBinaryOp, | ||
classifyImplicitName, | ||
fragileAlias, | ||
identAst, | ||
functionAst, | ||
setLastAsXpr, | ||
xprToken, | ||
valuePathAst, | ||
signedExpression, | ||
numberLiteral, | ||
quotedLiteral, | ||
pathName, | ||
docComment, | ||
addDef, | ||
addItem, | ||
addExtension, | ||
createSource, | ||
createDict, | ||
createArray, | ||
finalizeDictOrArray, | ||
createPrefixOp, | ||
setOnce, | ||
setMaxCardinality, | ||
pushIdent, | ||
handleComposition, | ||
associationInSelectItem, | ||
reportExpandInline, | ||
checkTypeFacet, | ||
notSupportedYet, | ||
csnParseOnly, | ||
disallowElementExtension, | ||
noAssignmentInSameLine, | ||
noSemicolonHere, | ||
setLocalToken, | ||
setLocalTokenIfBefore, | ||
setLocalTokenForId, | ||
excludeExpected, | ||
isStraightBefore, | ||
meltKeywordToIdentifier, | ||
prepareGenericKeywords, | ||
reportErrorForGenericKeyword, | ||
parseMultiLineStringLiteral, | ||
}); | ||
GenericAntlrParser.prototype = Object.assign( | ||
Object.create( antlr4.Parser.prototype ), { | ||
message: function(...args) { return _message( this, 'message', ...args ); }, | ||
error: function(...args) { return _message( this, 'error', ...args ); }, | ||
warning: function(...args) { return _message( this, 'warning', ...args ); }, | ||
info: function(...args) { return _message( this, 'info', ...args ); }, | ||
attachLocation, | ||
startLocation, | ||
tokenLocation, | ||
valueWithTokenLocation, | ||
previousTokenAtLocation, | ||
combinedLocation, | ||
createDict, | ||
setDictEndLocation, | ||
surroundByParens, | ||
unaryOpForParens, | ||
leftAssocBinaryOp, | ||
classifyImplicitName, | ||
fragileAlias, | ||
identAst, | ||
functionAst, | ||
valuePathAst, | ||
signedExpression, | ||
numberLiteral, | ||
quotedLiteral, | ||
pathName, | ||
docComment, | ||
addDef, | ||
addItem, | ||
artifactForElementAnnotateOrExtend, | ||
assignProps, | ||
createPrefixOp, | ||
setOnce, | ||
setMaxCardinality, | ||
pushIdent, | ||
handleComposition, | ||
associationInSelectItem, | ||
reportExpandInline, | ||
checkTypeFacet, | ||
notSupportedYet, | ||
csnParseOnly, | ||
disallowElementExtension, | ||
noAssignmentInSameLine, | ||
noSemicolonHere, | ||
setLocalToken, | ||
setLocalTokenIfBefore, | ||
excludeExpected, | ||
isStraightBefore, | ||
meltKeywordToIdentifier, | ||
prepareGenericKeywords, | ||
parseMultiLineStringLiteral, | ||
constructor: GenericAntlrParser, // keep this last | ||
} | ||
); | ||
// Patterns for literal token tests and creation. The value is a map from the | ||
@@ -126,22 +127,23 @@ // `prefix` argument of function `quotedliteral` to the following properties: | ||
// - `literal`: the value which is used instead of `prefix` in the AST | ||
// - `normalized`: function called with argument `value`, return value is used | ||
// instead of `value` in the AST | ||
// TODO: think about laxer regexp for date/time/timestamp - normalization? | ||
// TODO: we might do a range check (consider leap seconds, i.e. max value 60), | ||
// but always allow Feb 29 (no leap year computation) | ||
// TODO: make it a configurable error (syntax-invalid-literal) | ||
// TODO: also use for CSN input | ||
const quotedLiteralPatterns = { | ||
x: { | ||
test_msg: 'A binary literal must have an even number of characters', | ||
test_variant: 'uneven-hex', | ||
test_fn: (str => Number.isInteger(str.length / 2)), | ||
unexpected_msg: 'A binary literal must only contain characters 0-9, a-f and A-F', | ||
unexpected_variant: 'invalid-hex', | ||
unexpected_char: /[^0-9a-f]/i, | ||
}, | ||
time: { | ||
test_msg: 'Expected time\'HH:MM:SS\' where H, M and S are numbers and \':SS\' is optional', | ||
test_variant: 'time', | ||
test_re: /^[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?$/, | ||
}, | ||
date: { | ||
test_msg: 'Expected date\'YYYY-MM-DD\' where Y, M and D are numbers', | ||
test_re: /^[0-9]{1,4}-[0-9]{1,2}-[0-9]{1,2}$/, | ||
test_variant: 'date', | ||
test_re: /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/, | ||
}, | ||
timestamp: { | ||
test_msg: 'Expected timestamp\'YYYY-MM-DD HH:MM:SS.u…u\' where Y, M, D, H, S and u are numbers (optional 1-7×u)', | ||
test_variant: 'timestamp', | ||
test_re: /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}(:[0-9]{2}(\.[0-9]{1,7})?)?$/, | ||
@@ -169,4 +171,4 @@ }, | ||
// Use the following function for language constructs which we (currently) do | ||
// not really compile, just use to produce a CSN for functions parseToCqn() and | ||
// parseToExpr(). | ||
// not really compile, just use to produce a CSN for functions parse.cql() and | ||
// parse.expr(). | ||
function csnParseOnly( text, ...tokens ) { | ||
@@ -225,2 +227,11 @@ if (!text || this.options.parseOnly) | ||
function setLocalTokenForId( tokenNameMap ) { | ||
const ll1 = this.getCurrentToken(); | ||
if (ll1.type === this.constructor.Identifier || /^[a-zA-Z_]+$/.test( ll1.text )) { | ||
const tokenName = tokenNameMap[ this._input.LT(2).text || '' ]; | ||
if (tokenName) | ||
ll1.type = this.constructor[tokenName]; | ||
} | ||
} | ||
// // Special function for rule `requiredSemi` before return $ctx | ||
@@ -283,21 +294,47 @@ // function braceForSemi() { | ||
function prepareGenericKeywords( pathItem ) { | ||
this.$genericKeywords = { argFull: [] }; | ||
if (!pathItem) | ||
return; | ||
const func = pathItem.id && specialFunctions[pathItem.id.toUpperCase()]; | ||
const spec = func && func[pathItem.args ? pathItem.args.length : 0]; | ||
if (!spec) | ||
return; | ||
// currently, we only have 'argFull', i.e. a keyword which is alternative to expression | ||
const genericTokenTypes = { | ||
expr: 'GenericExpr', | ||
separator: 'GenericSeparator', | ||
intro: 'GenericIntro', | ||
}; | ||
function prepareGenericKeywords( pathItem, expected = null) { | ||
const length = pathItem?.args?.length || 0; | ||
const argPos = (expected ? length -1 : length); | ||
const func = pathItem?.id && specialFunctions[pathItem.id.toUpperCase()]; | ||
const spec = func && func[argPos] || specialFunctions[''][argPos ? 1 : 0]; | ||
this.$genericKeywords = spec; | ||
// currently, we only have 'TODO', i.e. a keyword which is alternative to expression | ||
// TODO: If not just at the beginning, we need a stack for $genericKeywords, | ||
// as we can have nested special functions | ||
this.$genericKeywords.argFull = Object.keys( spec ); | ||
// @ts-ignore | ||
const token = this.getCurrentToken() || { text: '' }; | ||
if (spec[token.text.toUpperCase()] === 'argFull') { | ||
// @ts-ignore | ||
token.type = this.constructor.GenericArgFull; | ||
const text = token.text.toUpperCase(); | ||
let generic = spec[text]; | ||
// console.log('PGK:',token.text,generic,expected,spec,func,argPos) | ||
if (expected) { // 'separator' or 'expr' (after 'separator') | ||
if (generic !== expected) | ||
return; | ||
} | ||
else if (!generic || generic === 'separator') { | ||
// Mismatch at beginning (or just an expression): keep token type | ||
// (if not expression, issue error and consider the token to be an | ||
// expression replacement, like ALL) | ||
return; | ||
} | ||
else if (generic === 'expr' && spec.intro && spec.intro.includes( text )) { | ||
// token is both an intro and an expression, like LEADING for TRIM | ||
const next = this._input.LT(2).text; | ||
if (!next || // followed by EOF -> consider it to be 'intro', better for CC | ||
next !== ',' && next !== ')' && spec[next.toUpperCase()] !== 'separator') | ||
generic = 'intro'; // is intro if next token is not separator, not ',', ')' | ||
} | ||
// @ts-ignore | ||
token.type = this.constructor[genericTokenTypes[generic]]; | ||
} | ||
// To be called before having matched ( HideAlternatives | … ) | ||
function reportErrorForGenericKeyword() { | ||
this._errHandler.reportUnwantedToken( this ); | ||
//this._errHandler.reportInputMismatch( this, { offending: this._input.LT(1) }, null ); | ||
} | ||
@@ -319,2 +356,48 @@ // Attach location matched by current rule to node `art`. If a location is | ||
function assignAnnotation( art, anno, prefix = '', iHaveVariant ) { | ||
const { name, $flatten } = anno; | ||
const { path } = name; | ||
if (path.broken || !path[path.length - 1].id) | ||
return; | ||
const pathname = pathName( path ); | ||
let absolute = ''; | ||
if (name.variant) { | ||
absolute = `${ prefix }${ pathname }#${ name.variant.id }`; | ||
if (iHaveVariant) { // TODO: do we really care in the parser / core compiler? | ||
this.error( 'anno-duplicate-variant', [ name.variant.location ], | ||
{}, // TODO: params | ||
'Annotation variant has been already provided' ); | ||
} | ||
} | ||
else if (!prefix || pathname !== '$value') { | ||
absolute = `${ prefix }${ pathname }`; | ||
} | ||
else { | ||
absolute = prefix.slice( 0, -1 ); | ||
} | ||
if ($flatten) { | ||
for (const a of $flatten) | ||
this.assignAnnotation( art, a, `${ absolute }.`, iHaveVariant || name.variant); | ||
} | ||
else { | ||
name.absolute = absolute; | ||
const prop = '@' + absolute; | ||
const old = art[prop]; | ||
if (old && old.$inferred) | ||
art[prop] = anno; | ||
else | ||
dictAddArray( art, prop, anno, (n, location, a) => { | ||
this.error( 'syntax-duplicate-anno', [ location ], { anno: n }, | ||
'Duplicate assignment with $(ANNO)' ); | ||
a.$errorReported = true; // do not report again later as anno-duplicate-xyz | ||
} ); | ||
} | ||
if (!prefix) { // set deprecated $annnotations for cds-lsp | ||
if (!art.$annotations) | ||
art.$annotations = []; | ||
const location = locUtils.combinedLocation( anno.name, anno ); | ||
art.$annotations.push( { value: anno, location } ); | ||
} | ||
} | ||
/** | ||
@@ -419,16 +502,2 @@ * Return start location of `token`, or the first token matched by the current | ||
function createDict( location = null ) { | ||
const dict = Object.create(null); | ||
dict[$location] = location || this.startLocation( this._input.LT(-1) ); | ||
return dict; | ||
} | ||
function setDictEndLocation( dict ) { | ||
const stop = this._input.LT(-1); | ||
Object.assign( dict[$location], { | ||
endLine: stop.line, | ||
endCol: stop.stop - stop.start + stop.column + 2, | ||
} ); | ||
} | ||
function surroundByParens( expr, open, close, asQuery = false ) { | ||
@@ -499,6 +568,7 @@ if (!expr) | ||
// Return AST for identifier token `token`. Also check that identifer is not empty. | ||
function identAst( token, category ) { | ||
function identAst( token, category, noTokenTypeCheck = false ) { | ||
token.isIdentifier = category; | ||
let id = token.text; | ||
if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id )) | ||
if (!noTokenTypeCheck && | ||
token.type !== this.constructor.Identifier && !/^[a-zA-Z_]+$/.test( id )) | ||
id = ''; | ||
@@ -530,7 +600,7 @@ if (token.text[0] === '!') { | ||
function functionAst( token, xprToken ) { | ||
function functionAst( token, xpr ) { // only for special function TRIM and EXTRACT | ||
// TODO: XSN func cleanup | ||
const location = this.tokenLocation( token ); | ||
const args = xprToken | ||
? [ { op: { location, val: 'xpr' }, args: [], location: this.tokenLocation( xprToken ) } ] | ||
const args = xpr | ||
? [ { op: { location, val: 'ixpr' }, args: [], location: this.tokenLocation( xpr ) } ] | ||
: []; | ||
@@ -545,2 +615,17 @@ return { | ||
function setLastAsXpr( args ) { | ||
const pos = args.length - 1; | ||
const last = args[pos]; | ||
if (!last || last?.op?.val === 'ixpr') // actuall an internal xpr, which is always flattened | ||
return last; | ||
const location = { ...last.location }; | ||
args[pos] = { op: { location, val: 'ixpr' }, args: [ last ], location }; | ||
return args[pos].args; | ||
} | ||
function xprToken() { | ||
const token = this._input.LT(-1); | ||
return { location: this.tokenLocation( token ), val: token.text, literal: 'token' } | ||
} | ||
function valuePathAst( ref ) { | ||
@@ -644,6 +729,5 @@ // TODO: XSN representation of functions is a bit strange - rework if methods | ||
// TODO: make tests available for CSN parser | ||
if ((p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val)) && | ||
!this.options.parseOnly) | ||
this.error( null, location, p.test_msg ); // TODO: message id | ||
this.warning( 'syntax-invalid-literal', location, { '#': p.test_variant } ); | ||
@@ -653,3 +737,3 @@ if (p.unexpected_char) { | ||
if (~idx) { | ||
this.error( null, { // TODO: message id | ||
this.warning( 'syntax-invalid-literal', { | ||
file: location.file, | ||
@@ -660,3 +744,3 @@ line: location.line, | ||
endCol: atChar( idx + (val[idx] === '\'' ? 2 : 1) ), | ||
}, p.unexpected_msg ); | ||
}, { '#': p.unexpected_variant } ); | ||
} | ||
@@ -708,15 +792,12 @@ } | ||
// Add new definition to dictionary property `env` of node `parent` and return | ||
// that definition. Also attach the following properties to the new definition: | ||
// - `name`: argument `name`, which is used as key in the dictionary | ||
// - `kind`: argument `kind` if that is truthy | ||
// - `location`: argument `location` or the start location of source matched by | ||
// current rule | ||
// - properties in argument `props` which are no empty (undefined, null, {}, | ||
// []), ANTLR tokens are replaced by their locations | ||
// Add new definition `art` to dictionary property `env` of node `parent`. | ||
// Return `art`. | ||
// | ||
// Hack: if argument `location` is exactly `true`, do not set `location` | ||
// (except if part of `props`), but also include the empty properties of | ||
// `props`. | ||
function addDef( parent, env, kind, name, annos, props, location ) { | ||
// If argument `kind` is provided, set `art.kind` to that value. | ||
// If argument `name` is provided, set `art.name`: | ||
// - if `name` is an array, the name consist of the ID of the last path item | ||
// (for elements via columns, foreign keys, table aliases) | ||
// - if `name` is an object, the name consist of the IDs of all path items | ||
// (for main artifact definitions) | ||
function addDef( art, parent, env, kind, name ) { | ||
if (Array.isArray(name)) { | ||
@@ -726,3 +807,3 @@ // XSN TODO: clearly say: definitions have name.path, members have name.id | ||
if (last && last.id) { // // A.B.C -> 'C' | ||
name = { | ||
art.name = { | ||
id: last.id, location: last.location, $inferred: 'as', | ||
@@ -732,10 +813,11 @@ }; | ||
} | ||
else if (name && name.id == null) { | ||
name.id = pathName( name.path ); // A.B.C -> 'A.B.C' | ||
else if (name) { | ||
art.name = name; | ||
if (name.id == null) | ||
name.id = pathName( name.path ); // A.B.C -> 'A.B.C' | ||
// TODO: get rid of setting `id`, only use for named values in structs | ||
} | ||
const art = this.assignProps( { name }, annos, props, location ); | ||
if (kind) | ||
art.kind = kind; | ||
if (!parent[env]) // TODO: dump with --test-mode, env !== 'artifacts' | ||
parent[env] = env === 'args' ? Object.create(null) : this.createDict( { ...location } ); | ||
if (!art.name || art.name.id == null) { | ||
@@ -745,5 +827,8 @@ // no id was parsed, but with error recovery: no further error | ||
// which could be tested in name search (then no undefined-ref error) | ||
// console.log( kind+': !!', art, parent, env, name ) | ||
return art; | ||
} | ||
else if (env === 'artifacts' || env === 'vocabularies') { | ||
// console.log( kind+':', art, parent, env, name ) | ||
if (env === 'artifacts' || env === 'vocabularies') { | ||
dictAddArray( parent[env], art.name.id, art ); | ||
@@ -755,14 +840,14 @@ } | ||
else { | ||
dictAdd( parent[env], art.name.id, art, ( name, loc ) => { | ||
dictAdd( parent[env], art.name.id, art, ( duplicateName, loc ) => { | ||
// do not use function(), otherwise `this` is wrong: | ||
if (kind === 0) { | ||
this.error( 'duplicate-argument', loc, { name }, | ||
this.error( 'duplicate-argument', loc, { name: duplicateName }, | ||
'Duplicate value for parameter $(NAME)' ); | ||
} | ||
else if (kind === '') { | ||
this.error( 'duplicate-excluding', loc, { name, keyword: 'excluding' }, | ||
this.error( 'duplicate-excluding', loc, { name: duplicateName, keyword: 'excluding' }, | ||
'Duplicate $(NAME) in the $(KEYWORD) clause' ); | ||
} | ||
else { | ||
this.error( 'duplicate-prop', loc, { name }, | ||
this.error( 'duplicate-prop', loc, { name: duplicateName }, | ||
'Duplicate value for structure property $(NAME)' ); | ||
@@ -775,83 +860,76 @@ } | ||
// Add new definition to array property `env` of node `parent` and return | ||
// that definition. Also attach the following properties to the new definition: | ||
// - `kind`: argument `kind` if that is truthy | ||
// - `location`: argument `location` or the start location of source matched by | ||
// current rule | ||
// - properties in argument `props` which are no empty (undefined, null, {}, | ||
// []); ANTLR tokens are replaced by their locations | ||
// | ||
// Hack: if argument `location` is exactly `true`, do not set `location` | ||
// (except if part of `props`), but also include the empty properties of | ||
// `props`. | ||
function addItem( parent, env, kind, annos, props, location ) { | ||
const art = this.assignProps( {}, annos, props, location ); | ||
if (kind) | ||
art.kind = kind; | ||
if (!env) | ||
parent.push( art ); | ||
else if (!parent[env]) | ||
parent[env] = [ art ]; | ||
else | ||
parent[env].push( art ); | ||
// Add new definition `art` to array property `env` of node `parent`. | ||
// Also set `kind`. Returns `art`. | ||
function addItem( art, parent, env, kind ) { | ||
art.kind = kind; | ||
parent[env].push( art ); | ||
return art; | ||
} | ||
/** | ||
* For `annotate/extend E:elem.sub`, create the `elements` structure | ||
* that can be used by the core compiler to annotate/extend elements. | ||
* Add `annotate/extend Main.Artifact:elem.sub` to `‹xsn›.extensions`: | ||
* - the array item is an extend/annotate for `Main.Artifact`, | ||
* - for each path item in `elem.sub`, we add an `elements` property containing | ||
* one extend/annotate for the corresponding element | ||
* - The deepest extend/annotate is the object which is to be extended | ||
* | ||
* @param {string} kind Either `annotate` or `extend` | ||
* @param {object} artifact Main artifact that shall have `elements`. | ||
* @param {XSN.Path} elementPath Path as returned by `simplePath` token. | ||
* @param {object[]} annos Existing annotations that shall be added to the _last_ path step. | ||
* @param {XSN.Location} artifactLocation Start location of the `annotate` statement. | ||
* @returns {object} Deepest element | ||
*/ | ||
function artifactForElementAnnotateOrExtend(kind, artifact, elementPath, annos, artifactLocation ) { | ||
if (!Array.isArray(elementPath) || elementPath.broken || elementPath.length < 1) | ||
return artifact; | ||
* @param {object} ext The object containing the location and annotations for the extension. | ||
* @param {object} parent The parent containing the `extensions` property, i.e. the source. | ||
* @param {string} kind Either `annotate` or `extend`. | ||
* @param {object} artName The "name object" for `Main.Artifact`. | ||
* @param {XSN.Path} elemPath Path as returned by `simplePath` rule. | ||
*/ | ||
function addExtension( ext, parent, kind, artName, elemPath ) { | ||
const { location } = ext; | ||
if (!Array.isArray( elemPath ) || !elemPath.length || elemPath.broken) { | ||
ext.name = artName; | ||
this.addItem( ext, parent, 'extensions', kind ); | ||
return; | ||
} | ||
// Note: the element extensions share a common `location`, also with the | ||
// extension of the main artifact; its end location will usually set later | ||
parent = this.addItem( { name: artName, location }, parent, 'extensions', kind ); | ||
for (const seg of elementPath.slice(0, -1)) { | ||
artifact = this.addDef( artifact, 'elements', kind, | ||
{ path: [seg], location: seg.location }, null, {}, artifactLocation ); | ||
const last = elemPath[elemPath.length - 1]; | ||
for (const seg of elemPath) { | ||
parent.elements = Object.create(null); // no dict location → no createDict() | ||
parent = this.addDef( (seg === last ? ext : { location }), | ||
parent, 'elements', kind, seg ); | ||
} | ||
const last = elementPath[elementPath.length - 1]; | ||
artifact = this.addDef( artifact, 'elements', kind, | ||
{ path: [ last ], location: last.location }, annos, {}, artifactLocation ); | ||
return artifact; | ||
} | ||
/** Assign all non-empty (undefined, null, {}, []) properties in argument | ||
* `props` and argument `annos` as property `$annotations` to `target` | ||
* and return it. Hack: if argument `annos` is exactly `true`, return | ||
* `Object.assign( target, props )`, for rule `namedValue`. ANTLR tokens are | ||
* replaced by their locations. | ||
* | ||
* @param {any} target | ||
* @param {any[]|true} [annos=[]] | ||
* @param {any} [props] | ||
* @param {any} [location] | ||
*/ | ||
function assignProps( target, annos = [], props = null, location = null) { | ||
if (annos === true) | ||
return Object.assign( target, props ); | ||
target.location = location || this.startLocation( this._ctx.start ); | ||
// Object.assign without "empty" elements/properties and with mappings: | ||
// - token instanceof antlr4.CommonToken => location of token | ||
for (const key in props) { | ||
let val = props[key]; | ||
if (val instanceof antlr4.CommonToken) | ||
val = this.valueWithTokenLocation( true, val); | ||
// only copy properties which are not undefined, null, {} or [] | ||
if (val != null && | ||
(typeof val !== 'object' || | ||
(Array.isArray(val) ? val.length : Object.getOwnPropertyNames(val).length) ) ) | ||
target[key] = val; | ||
} | ||
if (annos) | ||
target.$annotations = annos; | ||
return target; | ||
// must be in action directly after having parsed '{' or '(` | ||
function createDict() { | ||
const dict = Object.create(null); | ||
dict[$location] = this.startLocation( this._input.LT(-1) ); | ||
return dict; | ||
} | ||
// must be in action directly after having parsed '[' or '(` or `{` | ||
function createArray() { | ||
const array = []; | ||
array[$location] = this.startLocation( this._input.LT(-1) ); | ||
return array; | ||
} | ||
// must be in action directly after having parsed '}' or ')` | ||
function finalizeDictOrArray( dict ) { | ||
const loc = dict[$location]; | ||
if (!loc) | ||
return; | ||
const stop = this._input.LT(-1); | ||
loc.endLine = stop.line; | ||
loc.endCol = stop.stop - stop.start + stop.column + 2; | ||
} | ||
function createSource() { | ||
return { | ||
kind: 'source', | ||
usings: [], | ||
dependencies: [], | ||
artifacts: Object.create(null), | ||
// vocabularies: Object.create(null), | ||
extensions: [], | ||
}; | ||
} | ||
// Create AST node for prefix operator `op` and arguments `args` | ||
@@ -902,10 +980,8 @@ function createPrefixOp( token, args ) { | ||
function setMaxCardinality( art, token, max, inferred ) { | ||
function setMaxCardinality( art, token, max ) { | ||
const location = this.tokenLocation( token ); | ||
if (!art.cardinality) { | ||
art.cardinality = { targetMax: Object.assign( { location }, max ), location }; | ||
if (inferred) | ||
art.cardinality.$inferred = inferred; | ||
} | ||
else if (!inferred) { | ||
else { | ||
this.warning( 'syntax-repeated-cardinality', location, { keyword: token.text }, | ||
@@ -982,4 +1058,2 @@ 'The target cardinality has already been specified - ignored $(KEYWORD)' ); | ||
module.exports = { | ||
genericAntlrParser: GenericAntlrParser, | ||
}; | ||
module.exports = GenericAntlrParser; |
'use strict'; | ||
const whitespaceRegEx = /[\t\u{000B}\u{000C} \u{00A0}\u{FEFF}\p{Zs}]/u; | ||
const newLineRegEx = /\r\n?|\n|\u2028|\u2029/u; | ||
const { | ||
isWhitespaceOrNewLineOnly, | ||
isWhitespaceCharacterNoNewline, | ||
cdlNewLineRegEx, | ||
} = require('./textUtils'); | ||
/** | ||
* Returns true if the given string only contains whitespace characters. | ||
* | ||
* @todo Combine with function from docCommentParser | ||
* @param {string} str | ||
* @returns {boolean} | ||
*/ | ||
function isWhiteSpaceOnly(str) { | ||
return /^\s*$/.test(str); | ||
} | ||
/** | ||
* Check whether the given character is a white-space character as | ||
* defined by §11.2 of the ECMAScript 2020 specification. | ||
* See <https://262.ecma-international.org/11.0/#sec-white-space>. | ||
* | ||
* | Code Point | Name | Abbreviation | | ||
* |:--------------------|:-----------------------------------------------|--------------| | ||
* | U+0009 | CHARACTER TABULATION | `<TAB>` | | ||
* | U+000B | LINE TABULATION | `<VT>` | | ||
* | U+000C | FORM FEED (FF) | `<FF>` | | ||
* | U+0020 | SPACE | `<SP>` | | ||
* | U+00A0 | NO-BREAK SPACE | `<NBSP>` | | ||
* | U+FEFF | ZERO WIDTH NO-BREAK SPACE | `<ZWNBSP>` | | ||
* | Other category “Zs” | Any other Unicode “Space_Separator” code point | `<USP>` | | ||
* | ||
* @param char | ||
* @returns {boolean} | ||
*/ | ||
function isWhitespaceCharacter(char) { | ||
return whitespaceRegEx.test(char); | ||
} | ||
/** | ||
* Strips and counts the indentation from the given string. | ||
* This function is similar to the one in docCommentParser.js, but | ||
* has special handling for the first and last line of the string. | ||
* | ||
@@ -61,6 +33,6 @@ * @example | ||
// Note: We have to check all newline characters, as the string is not normalized, yet. | ||
const lines = str.split(newLineRegEx); | ||
const lines = str.split(cdlNewLineRegEx); | ||
const n = lines.length; | ||
const hasTrailingLineBreak = newLineRegEx.test(str[str.length - 1]); | ||
const hasTrailingLineBreak = cdlNewLineRegEx.test(str[str.length - 1]); | ||
if (hasTrailingLineBreak) { | ||
@@ -77,3 +49,3 @@ // Shortcut: | ||
// even if blank. For all other lines, blank lines are ignored. | ||
if (isWhiteSpaceOnly(line) && index !== (n-1)) | ||
if (isWhitespaceOrNewLineOnly(line) && index !== (n-1)) | ||
return min; | ||
@@ -83,3 +55,3 @@ | ||
const length = Math.min(min, line.length); | ||
while (count < length && isWhitespaceCharacter(line[count])) { | ||
while (count < length && isWhitespaceCharacterNoNewline(line[count])) { | ||
count++; | ||
@@ -144,3 +116,3 @@ } | ||
// there is no text without at least one line break. | ||
if (!newLineRegEx.test(this.str)) { | ||
if (!cdlNewLineRegEx.test(this.str)) { | ||
const loc = this._locationForCharacters(this.end, 1); | ||
@@ -307,3 +279,3 @@ this.parser.message('syntax-invalid-text-block', loc); | ||
for (let j = 0; j < count; ++j) { | ||
if (!this._eos() && /^[0-9A-Fa-f]$/.test(this._lookahead())) { | ||
if (!this._eos() && /^[\dA-Fa-f]$/.test(this._lookahead())) { | ||
this._move(); | ||
@@ -338,3 +310,3 @@ codePoint += this._current(); | ||
while (!this._eos()) { | ||
if (/^[0-9A-Fa-f]$/.test(this._lookahead())) { | ||
if (/^[\dA-Fa-f]$/.test(this._lookahead())) { | ||
this._move(); | ||
@@ -341,0 +313,0 @@ codePoint += this._current(); |
@@ -90,2 +90,11 @@ // Official cds-compiler API. | ||
/** | ||
* When set to `true`, and the model contains an entity `sap.common.Languages` | ||
* with an element `code`, all generated texts entities additionally contain | ||
* an element `language` which is an association to `sap.common.Languages` | ||
* using element `locale`. | ||
* | ||
* @since v2.8.0 | ||
*/ | ||
addTextsLanguageAssoc?: boolean | ||
/** | ||
* Option for {@link compileSources}. If set, all objects inside the | ||
@@ -407,2 +416,6 @@ * provided sources dictionary are interpreted as XSN structures instead | ||
* | ||
* This function uses the direct value of USINGs (in CSN `"requires"`) for its dependency graph, | ||
* i.e. this function does not resolve USINGs. If a USING matches a key in sourcesDict, | ||
* we assume that it depends on that file (/sourcesDict entry). | ||
* | ||
* See function {@link compile} for the meaning of the argument `options`. If there | ||
@@ -586,3 +599,2 @@ * are parse or other compilation errors, throws an exception {@link CompilationError} | ||
* unset. | ||
*/ | ||
@@ -614,21 +626,2 @@ export function messageContext(sourceLines: string[], msg: CompileMessage, config?: { | ||
/** | ||
* Same as {@link to.edm} but expects a {@link for.odata} transformed CSN. | ||
* | ||
* Note that parameter `service` is the same as `ODataOptions.service`. | ||
* The latter is not used. OData specific options have an internal format. | ||
* | ||
* @deprecated Use {@link to.edm} instead. If it detects a pre-transformed CSN, it won't run for.odata(). | ||
*/ | ||
export function preparedCsnToEdm(csn: CSN, service: string, options: ODataOptions): object; | ||
/** | ||
* Same as {@link to.edm} but expects a {@link for.odata} transformed CSN. | ||
* | ||
* Note that parameter `service` is the same as `ODataOptions.service`. | ||
* The latter is not used. OData specific options have an outdated format. | ||
* | ||
* @deprecated Use {@link to.edmx} instead. If it detects a pre-transformed CSN, it won't run for.odata(). | ||
*/ | ||
export function preparedCsnToEdmx(csn: CSN, service: string, options: ODataOptions): string; | ||
export namespace parse { | ||
@@ -666,11 +659,2 @@ /** | ||
/** | ||
* @deprecated Use {@link parse.cql} instead. | ||
*/ | ||
export function parseToCqn(cql: string, filename?: string, options?: Options): CQN; | ||
/** | ||
* @deprecated Use {@link parse.expr} instead. | ||
*/ | ||
export function parseToExpr(expr: string, filename?: string, options?: Options): CXN; | ||
/** | ||
* @note Actual name is "for" which can't be used directly in the documentation | ||
@@ -820,3 +804,3 @@ * as it is a reserved name in TypeScript. | ||
*/ | ||
[namespace: string]: string | ||
namespace?: string | ||
} | ||
@@ -866,17 +850,14 @@ | ||
* | ||
* The above rules might differ for different SQL dialects. | ||
* Exceptions will be listed below. | ||
* | ||
* @param artifactName The fully qualified name of the artifact | ||
* @param sqlMapping The naming mode to use. See {@link SqlOptions.sqlMapping} for more details. | ||
* @param csn | ||
* @param sqlDialect The SQL dialect to use. See {@link SqlOptions.sqlDialect} for more details. | ||
* @returns {string} The resulting database name for (absolute) 'artifactName', depending on the current naming mode. | ||
* @since v2.1.0 | ||
*/ | ||
export function getArtifactCdsPersistenceName(artifactName: string, sqlMapping: string, csn: CSN): string; | ||
export function getArtifactCdsPersistenceName(artifactName: string, sqlMapping: string, csn: CSN, sqlDialect: string): string; | ||
/** | ||
* This is an old function signature. If it is used - with a namespace as the third argument - the result might be wrong, | ||
* since the `.` -> `_` conversion for 'quoted'/'hdbcds' is missing. | ||
* | ||
* @deprecated Use the other overload with CSN instead. | ||
*/ | ||
export function getArtifactCdsPersistenceName(artifactName: string, sqlMapping: string, namespace: string): string; | ||
/** | ||
* Return the resulting database element name for `elemName`, depending on the current | ||
@@ -889,7 +870,11 @@ * naming mode. | ||
* | ||
* The above rules might differ for different SQL dialects. | ||
* Exceptions will be listed below. | ||
* | ||
* @param elemName The name of the element. For structured elements, concat by dot, e.g. `sub.elem`. | ||
* @param sqlMapping The naming mode to use. See {@link SqlOptions.sqlMapping} for more details. | ||
* @param sqlDialect The SQL dialect to use. See {@link SqlOptions.sqlDialect} for more details. | ||
* @returns The resulting database element name for 'elemName', depending on the current naming mode. | ||
*/ | ||
export function getElementCdsPersistenceName(elemName: string, sqlMapping: string): string; | ||
export function getElementCdsPersistenceName(elemName: string, sqlMapping: string, sqlDialect: string): string; | ||
@@ -1014,3 +999,3 @@ /** | ||
/** | ||
* String representation of the message. May be a multi-line message in the future. | ||
* String representation of the message. It may be a multi-line message in the future. | ||
*/ | ||
@@ -1034,3 +1019,3 @@ message: string | ||
/** | ||
* Returns a human readable string of the compiler message. Uses {@link messageString} to render | ||
* Returns a human-readable string of the compiler message. Uses {@link messageString} to render | ||
* the message without filename normalization and without a message ID. | ||
@@ -1067,3 +1052,3 @@ */ | ||
* - `string` or `Buffer`: the file content | ||
* - `{ realname: fs.realpath(filename) }`: if filename is not canonicalized | ||
* - `{ realname: fs.realpath.native(filename) }`: if filename is not canonicalized | ||
*/ | ||
@@ -1070,0 +1055,0 @@ export type FileCache = Record<string, boolean | string | Buffer | { realname: string }>; |
183
lib/main.js
@@ -16,16 +16,15 @@ // Main entry point for the CDS Compiler | ||
const backends = require('./backends'); | ||
const { odata, cdl, sql, hdi, hdbcds, edm, edmx } = require('./api/main'); | ||
const { getArtifactDatabaseNameOf, getElementDatabaseNameOf } = require('./model/csnUtils'); | ||
const { traverseCsn } = require('./model/api'); | ||
const { createMessageFunctions, sortMessages, sortMessagesSeverityAware, deduplicateMessages } = require('./base/messages'); | ||
const { smartId, smartFuncId, delimitedId } = require('./sql-identifier'); | ||
const keywords = require( './base/keywords' ); | ||
const snapi = lazyload('./api/main'); | ||
const csnUtils = lazyload('./model/csnUtils'); | ||
const model_api = lazyload('./model/api'); | ||
const messages = lazyload('./base/messages'); | ||
const sqlIdentifier = lazyload('./sql-identifier'); | ||
const keywords = lazyload( './base/keywords' ); | ||
const parseLanguage = require('./language/antlrParser'); | ||
const { parseX, compileX, compileSyncX, compileSourcesX, InvocationError } = require('./compiler'); | ||
const { fns } = require('./compiler/shared'); | ||
const define = require('./compiler/define'); | ||
const { isInReservedNamespace } = require("./compiler/builtins"); | ||
const finalizeParseCdl = require('./compiler/finalize-parse-cdl'); | ||
const parseLanguage = lazyload('./language/antlrParser'); | ||
const compiler = lazyload('./compiler'); | ||
const shared = lazyload('./compiler/shared'); | ||
const define = lazyload('./compiler/define'); | ||
const builtins = lazyload("./compiler/builtins"); | ||
const finalizeParseCdl = lazyload('./compiler/finalize-parse-cdl'); | ||
@@ -37,14 +36,4 @@ // The compiler version (taken from package.json) | ||
const { | ||
CompilationError, | ||
messageString, | ||
messageStringMultiline, | ||
messageContext, | ||
hasErrors, | ||
explainMessage, | ||
hasMessageExplanation | ||
} = require('./base/messages'); | ||
const toCsn = lazyload('./json/to-csn') | ||
const { compactModel, compactQuery, compactExpr } = require('./json/to-csn') | ||
function parseCdl( cdlSource, filename, options = {} ) { | ||
@@ -55,3 +44,3 @@ options = Object.assign( {}, options, { parseCdl: true } ); | ||
const model = { sources, options, $functions: {}, $volatileFunctions: {} }; | ||
const messageFunctions = createMessageFunctions( options, 'parse', model ); | ||
const messageFunctions = messages.createMessageFunctions( options, 'parse', model ); | ||
model.$messageFunctions = messageFunctions; | ||
@@ -62,23 +51,23 @@ | ||
sources[filename] = xsn; | ||
fns( model ); | ||
shared.fns( model ); | ||
define( model ); | ||
finalizeParseCdl( model ); | ||
messageFunctions.throwWithError(); | ||
return compactModel( model ); | ||
return toCsn.compactModel( model ); | ||
} | ||
function parseCql( cdlSource, filename = '<query>.cds', options = {} ) { | ||
const messageFunctions = createMessageFunctions( options, 'parse' ); | ||
const messageFunctions = messages.createMessageFunctions( options, 'parse' ); | ||
const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ), | ||
messageFunctions, 'query' ); | ||
messageFunctions.throwWithError(); | ||
return compactQuery( xsn ); | ||
return toCsn.compactQuery( xsn ); | ||
} | ||
function parseExpr( cdlSource, filename = '<expr>.cds', options = {} ) { | ||
const messageFunctions = createMessageFunctions( options, 'parse' ); | ||
const messageFunctions = messages.createMessageFunctions( options, 'parse' ); | ||
const xsn = parseLanguage( cdlSource, filename, Object.assign( { parseOnly: true }, options ), | ||
messageFunctions, 'expr' ); | ||
messageFunctions.throwWithError(); | ||
return compactExpr( xsn ); | ||
return toCsn.compactExpr( xsn ); | ||
} | ||
@@ -91,62 +80,71 @@ | ||
version, | ||
compile: (...args) => compileX(...args).then( compactModel ), // main function | ||
compileSync: (filenames, dir, options, fileCache) => compactModel( compileSyncX(filenames, dir, options, fileCache) ), // main function | ||
compileSources: (sourcesDict, options) => compactModel( compileSourcesX(sourcesDict, options) ), // main function | ||
compile: (...args) => compiler.compileX(...args).then( toCsn.compactModel ), // main function | ||
compileSync: (filenames, dir, options, fileCache) => toCsn.compactModel( compiler.compileSyncX(filenames, dir, options, fileCache) ), // main function | ||
compileSources: (sourcesDict, options) => toCsn.compactModel( compiler.compileSourcesX(sourcesDict, options) ), // main function | ||
compactModel: csn => csn, // for easy v2 migration | ||
CompilationError, | ||
sortMessages, | ||
sortMessagesSeverityAware, | ||
deduplicateMessages, | ||
messageString, | ||
messageStringMultiline, | ||
messageContext, | ||
explainMessage, | ||
hasMessageExplanation, | ||
InvocationError, // TODO: make it no error if same file name is provided twice | ||
hasErrors, | ||
get CompilationError() { | ||
Object.defineProperty(this, "CompilationError", { | ||
value: messages.CompilationError, | ||
writable: false, | ||
configurable: false, | ||
enumerable: false | ||
}); | ||
return messages.CompilationError; | ||
}, | ||
sortMessages: (...args) => messages.sortMessages(...args), | ||
sortMessagesSeverityAware: (...args) => messages.sortMessagesSeverityAware(...args), | ||
deduplicateMessages: (...args) => messages.deduplicateMessages(...args), | ||
messageString: (...args) => messages.messageString(...args), | ||
messageStringMultiline: (...args) => messages.messageStringMultiline(...args), | ||
messageContext: (...args) => messages.messageContext(...args), | ||
explainMessage: (...args) => messages.explainMessage(...args), | ||
hasMessageExplanation: (...args) => messages.hasMessageExplanation(...args), | ||
get InvocationError() { | ||
Object.defineProperty(this, "InvocationError", { | ||
value: compiler.InvocationError, | ||
writable: false, | ||
configurable: false, | ||
enumerable: false | ||
}); | ||
return compiler.InvocationError; | ||
}, | ||
hasErrors: (...args) => messages.hasErrors(...args), | ||
// Backends | ||
// TODO: Expose when transformers are switched to CSN | ||
// toOdataWithCsn: backends.toOdataWithCsn, | ||
preparedCsnToEdmx : (csn, service, options) => { return backends.preparedCsnToEdmx(csn, service, options).edmx}, | ||
preparedCsnToEdm : (csn, service, options) => { return backends.preparedCsnToEdm(csn, service, options).edmj}, | ||
// additional API: | ||
parse: { cdl: parseCdl, cql: parseCql, expr: parseExpr }, // preferred names | ||
/** | ||
* @deprecated Use parse.cql instead | ||
*/ | ||
parseToCqn: parseCql, | ||
/** | ||
* @deprecated Use parse.expr instead | ||
*/ | ||
parseToExpr: parseExpr, // deprecated name | ||
parse: { cdl: (...args) => parseCdl(...args), cql: (...args) => parseCql(...args), expr: (...args) => parseExpr(...args) }, | ||
// SNAPI | ||
for: { odata }, | ||
for: { odata: (...args) => snapi.odata(...args) }, | ||
to: { | ||
cdl: Object.assign(cdl, { | ||
keywords: Object.freeze([ ...keywords.cdl ]), | ||
functions: Object.freeze([ ...keywords.cdl_functions ]), | ||
cdl: Object.assign((...args) => snapi.cdl(...args), { | ||
keywords: Object.freeze([ ...keywords.cdl ] ), | ||
functions: Object.freeze([ ...keywords.cdl_functions ] ), | ||
}), | ||
sql: Object.assign(sql, { | ||
sqlite: { keywords: Object.freeze([ ...keywords.sqlite ] )}, | ||
smartId, | ||
smartFunctionId: smartFuncId, | ||
delimitedId, | ||
sql: Object.assign((...args) => snapi.sql(...args), { | ||
sqlite: { | ||
keywords: Object.freeze([ ...keywords.sqlite ] ) | ||
}, | ||
smartId: (...args) => sqlIdentifier.smartId(...args), | ||
smartFunctionId: (...args) => sqlIdentifier.smartFuncId(...args), | ||
delimitedId: (...args) => sqlIdentifier.delimitedId(...args), | ||
}), | ||
hdi: Object.assign(hdi, { | ||
keywords: Object.freeze([ ...keywords.hana ]), | ||
hdi: Object.assign((...args) => snapi.hdi(...args), { | ||
migration: (...args) => snapi.hdi.migration(...args), | ||
keywords: Object.freeze([ ...keywords.hana ] ), | ||
}), | ||
hdbcds: Object.assign(hdbcds, { | ||
keywords: Object.freeze([ ...keywords.hdbcds ]), | ||
hdbcds: Object.assign((...args) => snapi.hdbcds(...args), { | ||
keywords: Object.freeze([ ...keywords.hdbcds ] ), | ||
}), | ||
edm, | ||
edmx | ||
edm: Object.assign((...args) => snapi.edm(...args), { | ||
all: (...args) => snapi.edm.all(...args) | ||
}), | ||
edmx: Object.assign((...args) => snapi.edmx(...args), { | ||
all: (...args) => snapi.edmx.all(...args) | ||
}), | ||
}, | ||
// Convenience for hdbtabledata calculation in @sap/cds | ||
getArtifactCdsPersistenceName: getArtifactDatabaseNameOf, | ||
getElementCdsPersistenceName: getElementDatabaseNameOf, | ||
getArtifactCdsPersistenceName: (...args) => csnUtils.getArtifactDatabaseNameOf(...args), | ||
getElementCdsPersistenceName: (...args) => csnUtils.getElementDatabaseNameOf(...args), | ||
// Other API functions: | ||
traverseCsn, | ||
traverseCsn: (...args) => model_api.traverseCsn(...args), | ||
@@ -156,8 +154,33 @@ // INTERNAL functions for the cds-lsp package and friends - before you use | ||
// new releases (even having the same major version): | ||
$lsp: { parse: parseX, compile: compileX, getArtifactName: a => a.name }, | ||
$lsp: { parse: (...args) => compiler.parseX(...args), compile: (...args) => compiler.compileX(...args), getArtifactName: a => a.name }, | ||
// CSN Model related functionality | ||
model: { | ||
isInReservedNamespace, | ||
isInReservedNamespace: (...args) => builtins.isInReservedNamespace(...args), | ||
}, | ||
}; | ||
/** | ||
* Load the module on-demand and not immediately. | ||
* | ||
* @param {string} moduleName Name of the module to load - like with require | ||
* @returns {object} A Proxy that handles the on-demand loading | ||
*/ | ||
function lazyload(moduleName) { | ||
let module; | ||
return new Proxy(((...args) => { | ||
if (!module) // eslint-disable-next-line global-require | ||
module = require(moduleName); | ||
if (module.apply && typeof module.apply === 'function') | ||
return module.apply(this, args); | ||
return module; // for destructured calls | ||
}), { | ||
get(target, name) { | ||
if (!module) // eslint-disable-next-line global-require | ||
module = require(moduleName); | ||
return module[name]; | ||
}, | ||
}); | ||
} |
@@ -330,3 +330,3 @@ // CSN functionality for resolving references | ||
return notFound; | ||
throw new ModelError( 'Undefined reference' ); | ||
throw new ModelError( `Unknown artifact reference: ${typeof ref !== 'string' ? JSON.stringify(ref.ref) : ref}` ); | ||
} | ||
@@ -333,0 +333,0 @@ |
@@ -8,2 +8,4 @@ 'use strict'; | ||
const { ModelError } = require("../base/error"); | ||
const { typeParameters } = require("../compiler/builtins"); | ||
const { forEach } = require('../utils/objectUtils'); | ||
const version = require('../../package.json').version; | ||
@@ -38,7 +40,10 @@ | ||
/** | ||
* Get utility functions for a given CSN. | ||
* Get utility functions for a given CSN. Re-exports functions of `csnRefs()`. | ||
* @param {CSN.Model} model (Compact) CSN model | ||
*/ | ||
function getUtils(model, universalReady) { | ||
const { artifactRef, inspectRef, effectiveType, getOrigin, targetAspect, getColumn, getElement, initDefinition } = csnRefs(model, universalReady); | ||
const _csnRefs = csnRefs(model, universalReady); | ||
const { artifactRef } = _csnRefs; | ||
/** Cache for getFinalBaseTypeWithProps(). Specific to the current model. */ | ||
const finalBaseTypeCache = Object.create(null); | ||
@@ -61,13 +66,6 @@ return { | ||
cloneWithTransformations, | ||
getFinalBaseType, | ||
inspectRef, | ||
artifactRef, | ||
effectiveType, | ||
getFinalBaseTypeWithProps, | ||
get$combined, | ||
getOrigin, | ||
getQueryPrimarySource, | ||
targetAspect, | ||
getColumn, | ||
getElement, | ||
initDefinition | ||
..._csnRefs, | ||
}; | ||
@@ -267,3 +265,3 @@ | ||
return obj.elements || | ||
(obj.type && ((getFinalTypeDef(obj.type).elements) || (obj.type.ref && getFinalBaseType(obj.type).elements))); | ||
(obj.type && ((getFinalTypeDef(obj.type).elements) || (obj.type.ref && getFinalBaseTypeWithProps(obj.type)?.elements))); | ||
} | ||
@@ -493,93 +491,118 @@ | ||
function _normalizeTypeRef(type) { | ||
if (type && typeof type === 'object' && type.ref?.length === 1) | ||
type = type.ref[0]; // simplify type: no element -> simple string can be used | ||
return type; | ||
} | ||
/** | ||
* Resolve to the final type of a type, that means follow type chains, references to other types or | ||
* elements a.s.o | ||
* Works for all kinds of types, strings as well as type objects. Strings need to be absolute type names. | ||
* Returns the final type as string (if it has a name, which is not always the case, think of embedded structures), | ||
* else the type object itself is returned. If a type is structured, you can navigate into it by providing a path, | ||
* e.g. given the following model | ||
* type bar: S.foo; | ||
* type s1 { | ||
* s: s2; | ||
* }; | ||
* type s2 { | ||
* u: type of S.e:t; | ||
* } | ||
* service S { | ||
* type foo: type of S.e:i.j1; | ||
* entity e { | ||
* key i: { j1: Integer }; | ||
* t: bar; | ||
* v: s1; | ||
* x: blutz.s.u; | ||
* }; | ||
* type blutz: S.e.v; | ||
* view V as select from e { | ||
* 1+1 as i: bar, | ||
* }; | ||
* type tt: type of V:i; | ||
* } | ||
* the following calls will all return 'cds.Integer' | ||
* getFinalBaseType('S.tt') | ||
* getFinalBaseType('S.e',['i','j1']) | ||
* getFinalBaseType('S.e',['t']) | ||
* getFinalBaseType('S.e',['x']) | ||
* getFinalBaseType('S.blutz',['s', 'u']) | ||
* Types are always resolved as far as possible. A type name which has no further definition is simply returned. | ||
* Composed types (structures, entities, views, ...) are returned as type objects, if not drilled down into | ||
* the elements. Path steps that have no corresponding element lead to 'undefined'. Refs to something that has | ||
* no type (e.g. expr in a view without explicit type) returns 'null' | ||
* Resolve to the final type of a type, that means follow type chains, references, etc. | ||
* Input is a type name, i.e. string, or type ref, i.e. `{ ref: [...] }`. | ||
* | ||
* @param {string|object} type Type - either string or ref | ||
* @param {CSN.Path} path | ||
* @param {WeakMap} [resolved=new WeakMap()] WeakMap containing already resolved refs - if a ref is not cached, it will be resolved JIT | ||
* @param {object} [cycleCheck] Dictionary to remember already resolved types - to be cycle-safe | ||
* @returns | ||
* Returns `null` if the type can't be resolved or if the referenced element has no type, | ||
* e.g. `typeof V:calculated`. | ||
* Otherwise, if scalar, returns an object that has a `type` property and all collected type | ||
* properties, or the type object with `elements` or `items` property if structured/arrayed. | ||
* | ||
* Caches type lookups. If the CSN changes drastically, you will need to re-call `csnUtils()` | ||
* and use the newly returned `getFinalBaseTypeWithProps()`. | ||
* | ||
* @param {string|object} type Type as string or type ref, i.e. `{ ref: [...] }` | ||
* @returns {object|null} | ||
*/ | ||
function getFinalBaseType(type, path = [], resolved = new WeakMap(), cycleCheck = undefined) { | ||
function getFinalBaseTypeWithProps(type) { | ||
type = _normalizeTypeRef(type); | ||
if (!type) | ||
return type; | ||
if (typeof(type) === 'string') { | ||
if (isBuiltinType(type)) // built-in type | ||
return type; | ||
if (cycleCheck) { | ||
let visited = path.length? type + ':' + path.join('.') : type; | ||
if (cycleCheck[visited]) | ||
throw new ModelError('Circular type chain on type ' + type); | ||
else | ||
cycleCheck[visited] = true; | ||
return null; | ||
// Nothing to copy from builtin type name. | ||
if (typeof type === 'string' && isBuiltinType( type )) { | ||
return { type }; | ||
} | ||
// We differentiate between ref and type to avoid collisions due to dict key. | ||
// Delimiter chosen arbitrarily; just one that is rarely used. | ||
const resolvedKey = (typeof type === 'object') ? `ref:${type.ref.join('\\')}` : `type:${type}`; | ||
if (finalBaseTypeCache[resolvedKey]) { | ||
if (finalBaseTypeCache[resolvedKey] === true) | ||
throw new ModelError(`Detected circular type reference; can't resolve: ${resolvedKey}`); | ||
return finalBaseTypeCache[resolvedKey]; | ||
} | ||
let typeRef = artifactRef(type); // throws if not found | ||
const isNonScalar = _cacheNonScalar(typeRef); | ||
if (isNonScalar) | ||
return finalBaseTypeCache[resolvedKey]; | ||
const props = {}; | ||
_copyTypeProps(props, typeRef); | ||
// If the resolved type is a builtin, stop and use its type arguments. | ||
type = _normalizeTypeRef(typeRef.type); | ||
if (typeof type === 'string' && isBuiltinType(type)) | ||
return _cacheResolved(props); | ||
// Set to true (before the recursive call) to avoid cyclic issues. | ||
finalBaseTypeCache[resolvedKey] = true; | ||
// Continue the search | ||
const finalBase = getFinalBaseTypeWithProps(type); | ||
if (!finalBase) // Reference has no proper type, e.g. due to `type of View:calculated`. | ||
return _cacheResolved(null); | ||
const nonScalar = _cacheNonScalar(finalBase); | ||
if (nonScalar) | ||
return finalBaseTypeCache[resolvedKey]; | ||
// If not a non-scalar, must be resolved type. | ||
_copyTypeProps(props, finalBase); | ||
_cacheResolved(props); | ||
return props; | ||
/** | ||
* Cache/Store the type props under the current `resolvedKey` in the `resolved` cache. | ||
* | ||
* @param {object} typeProps | ||
*/ | ||
function _cacheResolved(typeProps) { | ||
finalBaseTypeCache[resolvedKey] = typeProps; | ||
return typeProps; | ||
} | ||
/** | ||
* Structured or arrayed types are not followed further, so cache them. | ||
* | ||
* @param obj | ||
* @returns {boolean} True, if structured/arrayed/invalid, false if scalar. | ||
*/ | ||
function _cacheNonScalar(obj) { | ||
if (!obj) { // Reference has no proper type, e.g. due to `type of View:calculated`. | ||
_cacheResolved(null); | ||
return true; | ||
} | ||
else { | ||
cycleCheck = Object.create(null); | ||
if (obj.elements || obj.items) { | ||
_cacheResolved(obj); | ||
return true; | ||
} | ||
let definedType = model.definitions[type]; | ||
if (definedType && definedType.type) | ||
return getFinalBaseType(definedType.type, path, resolved, cycleCheck); | ||
else | ||
return getFinalBaseType(definedType, path, resolved, cycleCheck); | ||
return false; | ||
} | ||
else if (typeof(type) === 'object') { | ||
if (type.ref) { | ||
// assert type.ref instanceof Array && type.ref.length >= 1 | ||
const ref = resolved.has(type) ? resolved.get(type).art : artifactRef(type); | ||
return getFinalBaseType(ref, path, resolved, cycleCheck); | ||
} | ||
else if (type.elements) { | ||
if (path.length) { | ||
let [e, ...p] = path; | ||
return getFinalBaseType(type.elements[e], p, resolved, cycleCheck); | ||
/** | ||
* Copy type properties from source to target. Also copies `type`, `enum`, | ||
* and `localized` (if keepLocalized is true). Only copies from source, | ||
* if target does not have them. | ||
* | ||
* @param {object} target | ||
* @param {object} source | ||
*/ | ||
function _copyTypeProps(target, source) { | ||
target.type = source.type; | ||
const typeProps = [ ...typeParameters.list, 'enum', 'default', 'localized' ]; | ||
for (const param of typeProps) { | ||
if (target[param] === undefined && source[param] !== undefined) { | ||
target[param] = source[param]; | ||
} | ||
} | ||
else if (type.type) | ||
return (getFinalBaseType(type.type, path, resolved, cycleCheck)); | ||
else if (type.items) | ||
return type; | ||
else | ||
// TODO: this happens if we don't have a type, e.g. an expression in a select list | ||
// in a view without explicit type. Instead of returning null we might want to return | ||
// the object instead? | ||
return null; | ||
return target; | ||
} | ||
return type; | ||
} | ||
@@ -778,21 +801,2 @@ } | ||
// Like Object.assign() but copies also non enumerable properties | ||
function assignAll(target, ...sources) { | ||
sources.forEach(source => { | ||
const descriptors = Object.getOwnPropertyNames(source).reduce((propertyDescriptors, current) => { | ||
propertyDescriptors[current] = Object.getOwnPropertyDescriptor(source, current); | ||
return propertyDescriptors; | ||
}, {}); | ||
// by default, Object.assign copies enumerable Symbols too | ||
Object.getOwnPropertySymbols(source).forEach(sym => { | ||
const descriptor = Object.getOwnPropertyDescriptor(source, sym); | ||
if (descriptor.enumerable) { | ||
descriptors[sym] = descriptor; | ||
} | ||
}); | ||
Object.defineProperties(target, descriptors); | ||
}); | ||
return target; | ||
} | ||
/** | ||
@@ -905,8 +909,6 @@ * @param {CSN.Query} mainQuery | ||
function isEdmPropertyRendered(elementCsn, options) { | ||
if(options.toOdata) | ||
options = options.toOdata; | ||
// FKs are rendered in | ||
// V2/V4 flat: always on | ||
// V4 struct: on/off | ||
const renderForeignKey = (options.version === 'v4' && options.odataFormat === 'structured') ? !!options.odataForeignKeys : true; | ||
const renderForeignKey = (options.odataVersion === 'v4' && options.odataFormat === 'structured') ? !!options.odataForeignKeys : true; | ||
const isNotIgnored = !elementCsn.target ? !elementCsn['@cds.api.ignore'] : true; | ||
@@ -934,12 +936,15 @@ const isNavigable = elementCsn.target ? | ||
* | ||
* If the old function signature is used - with a namespace as the third argument - the result might be wrong, | ||
* since the '.' -> '_' conversion for quoted/hdbcds is missing. | ||
* The above rules might differ for different SQL dialects. | ||
* Exceptions will be listed below. | ||
* | ||
* @param {string} artifactName The fully qualified name of the artifact | ||
* @param {('plain'|'quoted'|'hdbcds')} sqlMapping The naming mode to use | ||
* @param {CSN.Model|string|undefined} csn | ||
* @param {'plain'|'quoted'|'hdbcds'|string} sqlMapping The naming mode to use | ||
* @param {CSN.Model} csn | ||
* @param {('sqlite'|'hana'|'plain'|string)} [sqlDialect='plain'] The SQL dialect to use | ||
* @returns {string} The resulting database name for (absolute) 'artifactName', depending on the current naming mode. | ||
*/ | ||
function getArtifactDatabaseNameOf(artifactName, sqlMapping, csn) { | ||
if(csn && typeof csn === 'object' && csn.definitions) | ||
// eslint-disable-next-line no-unused-vars | ||
function getArtifactDatabaseNameOf(artifactName, sqlMapping, csn, sqlDialect='plain') { | ||
if(csn && typeof csn === 'object' && csn.definitions) { | ||
isValidMappingDialectCombi(sqlDialect, sqlMapping) | ||
if (sqlMapping === 'quoted' || sqlMapping === 'hdbcds') { | ||
@@ -953,20 +958,4 @@ return getResultingName(csn, sqlMapping, artifactName); | ||
} | ||
else { | ||
console.error(`This invocation of "getArtifactCdsPersistenceName" is deprecated, as it doesn't produce correct output with definition names containing dots - please provide a CSN as the third parameter.`); | ||
if (sqlMapping === 'hdbcds') { | ||
if (csn) { | ||
const namespace = String(csn); | ||
return `${namespace}::${artifactName.substring(namespace.length + 1)}`; | ||
} | ||
return artifactName; | ||
} | ||
else if (sqlMapping === 'plain') { | ||
return artifactName.replace(/\./g, '_').toUpperCase(); | ||
} | ||
else if (sqlMapping === 'quoted') { | ||
return artifactName; | ||
} | ||
else { | ||
throw new Error('Unknown naming mode: ' + sqlMapping); | ||
} | ||
} else { | ||
throw new Error('A valid CSN model is required to correctly calculate the database name of an artifact.'); | ||
} | ||
@@ -1054,3 +1043,9 @@ } | ||
function isValidMappingDialectCombi(sqlDialect, sqlMapping) { | ||
if(sqlMapping === 'hdbcds' && sqlDialect !== 'hana') | ||
throw new Error(`sqlMapping "hdbcds" must only be used with sqlDialect "hana" - found: ${sqlDialect}`); | ||
return true; | ||
} | ||
/** | ||
@@ -1064,7 +1059,13 @@ * Return the resulting database element name for 'elemName', depending on the current | ||
* | ||
* The above rules might differ for different SQL dialects. | ||
* Exceptions will be listed below. | ||
* | ||
* @param {string} elemName The name of the element | ||
* @param {('plain'|'quoted'|'hdbcds')} sqlMapping The naming mode to use | ||
* @param {'plain'|'quoted'|'hdbcds'|string} sqlMapping The naming mode to use | ||
* @param {('sqlite'|'hana'|'plain'|string)} [sqlDialect='plain'] The SQL dialect to use | ||
* @returns {string} The resulting database element name for 'elemName', depending on the current naming mode. | ||
*/ | ||
function getElementDatabaseNameOf(elemName, sqlMapping) { | ||
// eslint-disable-next-line no-unused-vars | ||
function getElementDatabaseNameOf(elemName, sqlMapping, sqlDialect='plain') { | ||
isValidMappingDialectCombi(sqlMapping, sqlDialect) | ||
if (sqlMapping === 'hdbcds') { | ||
@@ -1185,81 +1186,2 @@ return elemName; | ||
/** | ||
* Merge multiple 'options' objects (from right to left, i.e. rightmost wins). Structured option values are | ||
* merged deeply. Structured option value from the right may override corresponding bool options on the left, | ||
* but no other combination of struct/scalar values is allowed. Array options are not merged, i.e. their | ||
* content is treated like scalars. | ||
* Returns a new options object. | ||
* | ||
* @param {...CSN.Options} optionsObjects | ||
* @return {CSN.Options} | ||
*/ | ||
function mergeOptions(...optionsObjects) { | ||
let result = {}; | ||
for (const options of optionsObjects) { | ||
if (options) | ||
result = mergeTwo(result, options, 'options'); | ||
} | ||
// Reverse the array to ensure that the rightmost option has priority | ||
const reversedOptions = [...optionsObjects].reverse(); // de-structure and create a new array, so reverse doesn't impact optionsObject | ||
const msgOptions = reversedOptions.find(opt => opt && Array.isArray(opt.messages)); | ||
if (msgOptions) { | ||
result.messages = msgOptions.messages; | ||
} | ||
return result; | ||
// Recursively used for scalars, too | ||
function mergeTwo(left, right, name) { | ||
let intermediateResult; | ||
// Copy left as far as required | ||
if (Array.isArray(left)) { | ||
// Shallow-copy left array | ||
intermediateResult = left.slice(); | ||
} else if (isObject(left)) { | ||
// Deep-copy left object (unless empty) | ||
intermediateResult = Object.keys(left).length ? mergeTwo({}, left, name) : {}; | ||
} else { | ||
// Just use left scalar | ||
intermediateResult = left; | ||
} | ||
// Check against improper overwriting | ||
if (isObject(left) && !Array.isArray(left) && (Array.isArray(right) || isScalar(right))) { | ||
throw new ModelError(`Cannot overwrite structured option "${name}" with array or scalar value`); | ||
} | ||
if ((isScalar(left) && typeof left !== 'boolean' || Array.isArray(left)) && isObject(right) && !Array.isArray(right)) { | ||
throw new ModelError(`Cannot overwrite non-boolean scalar or array option "${name}" with structured value`); | ||
} | ||
// Copy or overwrite properties from right to left | ||
if (Array.isArray(right)) { | ||
// Shallow-copy right array | ||
intermediateResult = right.slice(); | ||
} else if (isObject(right)) { | ||
// Object overwrites undefined, scalars and arrays | ||
if (intermediateResult === undefined || isScalar(intermediateResult) || Array.isArray(intermediateResult)) { | ||
intermediateResult = {}; | ||
} | ||
// Deep-copy right object into result | ||
for (let key of Object.keys(right)) { | ||
intermediateResult[key] = mergeTwo(intermediateResult[key], right[key], `${name}.${key}`); | ||
} | ||
} else { | ||
// Right scalar wins (unless undefined) | ||
intermediateResult = (right !== undefined) ? right : intermediateResult; | ||
} | ||
return intermediateResult; | ||
} | ||
// Return true if 'o' is a non-null object or array | ||
function isObject(o) { | ||
return typeof o === 'object' && o !== null | ||
} | ||
// Return true if 'o' is a non-undefined scalar | ||
function isScalar(o) { | ||
return o !== undefined && !isObject(o); | ||
} | ||
} | ||
/** | ||
* If the artifact with the name given is part of a context (or multiple), return the top-most context. | ||
@@ -1315,25 +1237,2 @@ * Else, return the artifact itself. Namespaces are not of concern here. | ||
// Return the name of the parent artifact of the artifact 'name' or | ||
// '' if there is no parent. | ||
function getParentNameOf(name) { | ||
return name.substring(0, name.lastIndexOf('.')); | ||
} | ||
// Return an array of parent names of 'name' (recursing into grand-parents) | ||
// Examples: | ||
// 'foo.bar.wiz' => [ 'foo.bar', 'foo' ] | ||
// 'foo' => [] | ||
// 'foo::bar.wiz' => 'foo::bar' | ||
// 'foo::bar' => [] | ||
function getParentNamesOf(name) { | ||
let remainder = name.slice(0, -getLastPartOf(name).length); | ||
if (remainder.endsWith('.')) { | ||
let parentName = remainder.slice(0, -1); | ||
return [parentName, ...getParentNamesOf(parentName)]; | ||
} else { | ||
return []; | ||
} | ||
} | ||
/** | ||
@@ -1389,11 +1288,13 @@ * Copy all annotations from 'fromNode' to 'toNode'. | ||
/** | ||
* Applies annotations from `csn.extensions` to definitions, i.e. top-level artifacts. | ||
* Does _not_ apply element/param/action/... annotations. | ||
* Applies annotations from `csn.extensions` to definitions and their elements. | ||
* | ||
* `config.filter` can be used to only copy annotations for those definitions, | ||
* for which the filter returns true. | ||
* | ||
* @todo Does _not_ apply param/action/... annotations. | ||
* | ||
* @param {CSN.Model} csn | ||
* @param {{overwrite?: boolean, filter?: (name: string) => boolean}} config | ||
*/ | ||
function applyDefinitionAnnotationsFromExtensions(csn, config) { | ||
function applyAnnotationsFromExtensions(csn, config) { | ||
if (!csn.extensions) | ||
@@ -1405,6 +1306,27 @@ return; | ||
const name = ext.annotate || ext.extend; | ||
if (name && csn.definitions[name] && filter(name)) { | ||
copyAnnotationsAndDoc(ext, csn.definitions[name], config.overwrite); | ||
const def = csn.definitions[name]; | ||
if (name && def && filter(name)) { | ||
copyAnnotationsAndDoc(ext, def, config.overwrite); | ||
applyAnnotationsToElements(ext, def); | ||
} | ||
} | ||
function applyAnnotationsToElements(ext, def) { | ||
// Only the definition is arrayed but the extension is not since | ||
// `items` is not expected in `extensions` by the CSN frontend and not | ||
// generated by the CDL parser for `annotate E:arrayed.elem`. | ||
if (def.items) | ||
def = def.items; | ||
if (!ext.elements || !def.elements) | ||
return; | ||
forEach(ext.elements, (key, sourceElem) => { | ||
const targetElem = def.elements[key]; | ||
if (targetElem) { | ||
copyAnnotationsAndDoc(sourceElem, targetElem, config.overwrite); | ||
applyAnnotationsToElements(sourceElem, targetElem); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -1416,21 +1338,2 @@ | ||
// For each property named 'path' in 'node' (recursively), call callback(path, node) | ||
function forEachPath(node, callback) { | ||
if (node === null || typeof node !== 'object') { | ||
// Primitive node | ||
return; | ||
} | ||
for (let name in node) { | ||
if (!Object.hasOwnProperty.call( node, name )) | ||
continue; | ||
// If path found within a non-dictionary, call callback | ||
if (name === 'path' && Object.getPrototypeOf(node)) { | ||
callback(node.path, node); | ||
} | ||
// Descend recursively | ||
forEachPath(node[name], callback); | ||
} | ||
} | ||
/** | ||
@@ -1512,12 +1415,2 @@ * Return true if the artifact has a valid, truthy persistence.exists/skip annotation | ||
/** | ||
* Check whether the artifact is @cds.persistence.skip | ||
* | ||
* @param {CSN.Artifact} artifact | ||
* @returns {Boolean} | ||
*/ | ||
function isSkipped(artifact) { | ||
return hasAnnotationValue(artifact, '@cds.persistence.skip', true) | ||
} | ||
/** | ||
* Walk path in the CSN and return the result. | ||
@@ -1609,4 +1502,3 @@ * | ||
isBuiltinType, | ||
assignAll, | ||
applyDefinitionAnnotationsFromExtensions, | ||
applyAnnotationsFromExtensions, | ||
forEachGeneric, | ||
@@ -1631,7 +1523,4 @@ forEachDefinition, | ||
getNormalizedQuery, | ||
mergeOptions, | ||
getRootArtifactName, | ||
getLastPartOfRef, | ||
getParentNamesOf, | ||
getParentNameOf, | ||
getLastPartOf, | ||
@@ -1641,3 +1530,2 @@ copyAnnotations, | ||
isAspect, | ||
forEachPath, | ||
hasValidSkipOrExists, | ||
@@ -1647,3 +1535,2 @@ getNamespace, | ||
getServiceNames, | ||
isSkipped, | ||
walkCsnPath, | ||
@@ -1650,0 +1537,0 @@ getVariableReplacement, |
@@ -17,2 +17,3 @@ // Make internal properties of the XSN / augmented CSN visible | ||
const $inferred = Symbol.for('cds.$inferred'); | ||
const $location = Symbol.for('cds.$location'); | ||
@@ -100,2 +101,3 @@ class NOT_A_DICTIONARY {} // used for consol.log display | ||
_status: primOrString, // is a string anyway | ||
$annotations: as => as.map( $annotation ), | ||
$messageFunctions: () => '‹some functions›', | ||
@@ -154,3 +156,3 @@ } | ||
const name = reveal( node, parent ); | ||
if (name && typeof name === 'object' && name.absolute) { | ||
if (name && typeof name === 'object' && name.absolute && node.kind) { | ||
const text = artifactIdentifier( parent ); | ||
@@ -184,7 +186,16 @@ delete name.absolute; | ||
function $annotation( anno ) { // property for cds-lsp | ||
const { name, $flatten } = anno.value || anno; | ||
const value = ($flatten) | ||
? { name: reveal( name ), $flatten: $flatten.map( $annotation ) } | ||
: `@${name?.absolute}`; | ||
return { value, location: locationString( anno.location || anno.name.location ) }; | ||
} | ||
function columns( nodes, query ) { | ||
// If we will have specified elements, we need another test to see columns in --parse-cdl | ||
return nodes && nodes.map( c => (c._parent && c._parent.elements) | ||
? artifactIdentifier( c, query ) | ||
: reveal( c, nodes ) ); | ||
return nodes && array( nodes, | ||
c => (c._parent && c._parent.elements) | ||
? artifactIdentifier( c, query ) | ||
: reveal( c, nodes ) ); | ||
} | ||
@@ -222,3 +233,3 @@ | ||
if (Array.isArray(node)) // with args | ||
return node.map( n => reveal( n, node ) ); | ||
return array( node, reveal ); | ||
// Make unexpected prototype visible with node-10+: | ||
@@ -233,2 +244,4 @@ const r = Object.create( Object.getPrototypeOf(node) | ||
r['[$inferred]'] = node[$inferred]; | ||
if (node[$location] && !node['[$location]']) | ||
r['[$location]'] = locationString( node[$location] ); | ||
return r; | ||
@@ -248,3 +261,3 @@ } | ||
if (Array.isArray(node)) | ||
return node.map( n => revealNonEnum( n, node ) ); | ||
return array( node, revealNonEnum ); | ||
@@ -260,3 +273,3 @@ if (Object.getPrototypeOf( node )) | ||
if (Array.isArray(node)) | ||
return node.map( n => reveal( n, node, name ) ); | ||
return array( node, n => reveal( n, node, name ) ); | ||
@@ -291,2 +304,9 @@ const asLinkTest = kindsRepresentedAsLinks[ node.kind ]; | ||
function array( node, fn ) { | ||
const r = node.map( n => fn( n, node ) ); | ||
if (node[$location]) | ||
r.push( { $location: locationString( node[$location] ) } ); | ||
return r; | ||
} | ||
function artifactIdentifier( node, parent ) { | ||
@@ -347,3 +367,3 @@ if (Array.isArray(node)) | ||
if (Array.isArray(node)) | ||
return node.map( primOrString ); | ||
return array( node, primOrString ); | ||
if (Object.getPrototypeOf( node )) | ||
@@ -350,0 +370,0 @@ return '' + node; |
@@ -24,3 +24,3 @@ 'use strict'; | ||
const layers = []; | ||
let { zero, nonZero } = calculateDepth(Object.entries(csn.definitions)); | ||
let { zero, nonZero } = _calculateDepth(Object.entries(csn.definitions), _dependents, _dependencies); | ||
while (zero.length !== 0){ | ||
@@ -38,42 +38,42 @@ const currentLayer = []; | ||
layers.push(currentLayer); | ||
({zero, nonZero} = findWithXPointers(nonZero, 0)); | ||
({zero, nonZero} = _findWithXPointers(nonZero, 0, _dependents, _dependencies)); | ||
} | ||
return { layers, leftover: nonZero }; | ||
} | ||
function calculateDepth(definitionsArray) { | ||
const zero = []; | ||
const nonZero = []; | ||
function _calculateDepth(definitionsArray, _dependents, _dependencies) { | ||
const zero = []; | ||
const nonZero = []; | ||
definitionsArray.forEach(([artifactName, artifact]) => { | ||
if(artifact[_dependencies]) { | ||
artifact.$pointers = artifact[_dependencies].size; | ||
nonZero.push([artifactName, artifact]); | ||
} else { | ||
artifact.$pointers = 0; | ||
zero.push([artifactName, artifact]); | ||
} | ||
}); | ||
return { | ||
zero, | ||
nonZero | ||
definitionsArray.forEach(([artifactName, artifact]) => { | ||
if(artifact[_dependencies]) { | ||
artifact.$pointers = artifact[_dependencies].size; | ||
nonZero.push([artifactName, artifact]); | ||
} else { | ||
artifact.$pointers = 0; | ||
zero.push([artifactName, artifact]); | ||
} | ||
}); | ||
return { | ||
zero, | ||
nonZero | ||
} | ||
} | ||
function findWithXPointers(definitionsArray, x=0){ | ||
const zero = []; | ||
const nonZero = []; | ||
function _findWithXPointers(definitionsArray, x, _dependents, _dependencies){ | ||
const zero = []; | ||
const nonZero = []; | ||
definitionsArray.forEach(([artifactName, artifact]) => { | ||
if(artifact.$pointers !== undefined && artifact.$pointers === x) { | ||
zero.push([artifactName, artifact]); | ||
} else { | ||
nonZero.push([artifactName, artifact]); | ||
} | ||
}); | ||
definitionsArray.forEach(([artifactName, artifact]) => { | ||
if(artifact.$pointers !== undefined && artifact.$pointers === x) { | ||
zero.push([artifactName, artifact]); | ||
} else { | ||
nonZero.push([artifactName, artifact]); | ||
} | ||
}); | ||
return { | ||
zero, | ||
nonZero | ||
} | ||
return { | ||
zero, | ||
nonZero | ||
} | ||
@@ -88,2 +88,3 @@ } | ||
* @param {object} sql Map of <object name>: "CREATE STATEMENT" | ||
* @param {CSN.Model} csn | ||
* | ||
@@ -90,0 +91,0 @@ * @returns {{name: string, sql: string}[]} Sorted array of artifact name / "CREATE STATEMENTS" pairs |
@@ -37,3 +37,2 @@ 'use strict'; | ||
.option(' --deprecated <list>') | ||
.option(' --hana-flavor') | ||
.option(' --direct-backend') | ||
@@ -111,3 +110,2 @@ .option(' --parse-only') | ||
Valid values are: | ||
addTextsLanguageAssoc | ||
hanaAssocRealCardinality | ||
@@ -118,11 +116,3 @@ mapAssocToJoinCardinality | ||
Valid values are: | ||
noElementsExpansion | ||
v1KeysForTemporal | ||
parensAsStrings | ||
projectionAsQuery | ||
renderVirtualElements | ||
unmanagedUpInComponent | ||
createLocalizedViews | ||
redirectInSubQueries | ||
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete) | ||
eagerPersistenceForGeneratedEntities | ||
--parse-only Stop compilation after parsing and write result to <stdout> | ||
@@ -213,3 +203,3 @@ --fallback-parser <type> If the language cannot be deduced by the file's extensions, use this | ||
.option('-h, --help') | ||
.option('-v, --version <version>', ['v2', 'v4', 'v4x']) | ||
.option('-v, --odata-version <version>', ['v2', 'v4', 'v4x']) | ||
.option('-x, --xml') | ||
@@ -233,6 +223,6 @@ .option('-j, --json') | ||
-h, --help Show this help text | ||
-v, --version <version> ODATA version | ||
v2: ODATA V2 | ||
v4: (default) ODATA V4 | ||
v4x: { version: 'v4', odataContainment:true, format:'structured' } | ||
-v, --odata-version <version> ODATA version | ||
v2: ODATA V2 | ||
v4: (default) ODATA V4 | ||
v4x: { version: 'v4', odataContainment:true, format:'structured' } | ||
-x, --xml (default) Generate XML output (separate or combined) | ||
@@ -258,3 +248,3 @@ -j, --json Generate JSON output as "<svc>.json" (not available for v2) | ||
-s, --service-names <list> List of comma-separated service names to be rendered | ||
(default) empty, all services are rendered | ||
(default) empty, all services are rendered | ||
`); | ||
@@ -278,3 +268,3 @@ | ||
.option(' --joinfk') | ||
.option('-d, --dialect <dialect>', ['hana', 'sqlite', 'plain']) | ||
.option('-d, --dialect <dialect>', ['hana', 'sqlite', 'plain', 'postgres']) | ||
.option('-u, --user <user>') | ||
@@ -310,5 +300,6 @@ .option('-l, --locale <locale>') | ||
-d, --dialect <dialect> SQL dialect to be generated: | ||
plain : (default) Common SQL - no assumptions about DB restrictions | ||
hana : SQL with HANA specific language features | ||
sqlite : Common SQL for sqlite | ||
plain : (default) Common SQL - no assumptions about DB restrictions | ||
hana : SQL with HANA specific language features | ||
sqlite : Common SQL for sqlite | ||
postgres : Common SQL for postgres - beta-feature | ||
-u, --user <user> Value for the "$user" variable | ||
@@ -315,0 +306,0 @@ -l, --locale <locale> Value for the "$user.locale" variable in "sqlite"/"plain" dialect |
@@ -19,4 +19,4 @@ { | ||
"env": { | ||
"es6": true | ||
"es2020": true | ||
} | ||
} |
@@ -5,4 +5,4 @@ /** | ||
* then call addElement to register the elements of the current artifact. | ||
* Finally call the "done" function to check for duplicates. | ||
* In addition the internal structures will be reinitialized to enable reuse of the instance. | ||
* Finally, call the "done" function to check for duplicates. | ||
* In addition, the internal structures will be reinitialized to enable reuse of the instance. | ||
*/ | ||
@@ -12,2 +12,4 @@ | ||
const { forEach } = require('../utils/objectUtils'); | ||
/** | ||
@@ -126,7 +128,2 @@ * database name - uppercase if not quoted | ||
function forEach(obj, callback) { | ||
for (const key in obj) | ||
callback(key, obj[key]); | ||
} | ||
module.exports = DuplicateChecker; |
@@ -9,2 +9,5 @@ | ||
const { forEach } = require('../utils/objectUtils'); | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { optionProcessor } = require('../optionProcessor'); | ||
const { transformForHanaWithCsn } = require('../transform/forHanaNew'); | ||
@@ -16,2 +19,66 @@ const { | ||
/** | ||
* Used only by `cdsc manageConstraints`. | ||
* Not part of our API, yet. | ||
* | ||
* @param {CSN.Model} csn | ||
* @param {CSN.Options} options | ||
*/ | ||
function alterConstraintsWithCsn(csn, options) { | ||
const { error } = makeMessageFunction(csn, options, 'manageConstraints'); | ||
const { | ||
drop, alter, names, src, violations, | ||
} = options.manageConstraints || {}; | ||
if (drop && alter) | ||
error(null, null, 'Option “--drop” can\'t be combined with “--alter”'); | ||
options.sqlDialect = 'hana'; | ||
options.sqlMapping = names || 'plain'; | ||
// Of course we want the database constraints | ||
options.assertIntegrityType = 'DB'; | ||
const transformedOptions = _transformSqlOptions(csn, options); | ||
const forSqlCsn = transformForHanaWithCsn(csn, transformedOptions, 'to.sql'); | ||
if (violations && src && src !== 'sql') | ||
error(null, null, `Option “--violations“ can't be combined with source style “${src}“`); | ||
let intermediateResult; | ||
if (violations) | ||
intermediateResult = listReferentialIntegrityViolations(forSqlCsn, transformedOptions); | ||
else | ||
intermediateResult = manageConstraints(forSqlCsn, transformedOptions); | ||
return intermediateResult; | ||
} | ||
function _transformSqlOptions(model, options) { | ||
// Merge options with defaults. | ||
options = Object.assign({ sqlMapping: 'plain', sqlDialect: 'plain' }, options); | ||
options.toSql = true; | ||
if (!options.src && !options.csn) | ||
options.src = 'sql'; | ||
const { warning, error } = makeMessageFunction(model, options, 'to.sql'); | ||
optionProcessor.verifyOptions(options, 'toSql', true) | ||
.forEach(complaint => warning(null, null, `${complaint}`)); | ||
if (options.sqlDialect !== 'hana') { | ||
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only | ||
if (options.sqlMapping === 'quoted' || options.sqlMapping === 'hdbcds') | ||
error(null, null, `Option "{ sqlDialect: '${options.sqlDialect}' }" can't be combined with "{ sqlMapping: '${options.sqlMapping}' }"`); | ||
// No non-HANA SQL for HDI | ||
if (options.src === 'hdi') | ||
error(null, null, `Option "{ sqlDialect: '${options.sqlDialect}' }" can't be used for HDI"`); | ||
} | ||
return options; | ||
} | ||
/** | ||
* This render middleware can be used to generate SQL DDL ALTER TABLE <table> ALTER / ADD / DROP CONSTRAINT <constraint> statements for a given CDL model. | ||
@@ -43,3 +110,3 @@ * Moreover, it can be used to generate .hdbconstraint artifacts. | ||
let alterTableStatement = ''; | ||
alterTableStatement += `${indent}ALTER TABLE ${quoteSqlId(getResultingName(csn, options.toSql.names, constraint.dependentTable))}`; | ||
alterTableStatement += `${indent}ALTER TABLE ${quoteSqlId(getResultingName(csn, options.sqlMapping, constraint.dependentTable))}`; | ||
if (renderAlterConstraintStatement) | ||
@@ -178,3 +245,3 @@ alterTableStatement += `\n${indent}ALTER ${renderedConstraint};`; | ||
function quoteAndGetResultingName(id) { | ||
return quoteSqlId(getResultingName(csn, options.toSql.names, id)); | ||
return quoteSqlId(getResultingName(csn, options.sqlMapping, id)); | ||
} | ||
@@ -199,4 +266,5 @@ | ||
module.exports = { | ||
alterConstraintsWithCsn, | ||
manageConstraints, | ||
listReferentialIntegrityViolations, | ||
}; |
'use strict'; | ||
const { CompilationError, hasErrors } = require('../base/messages'); | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { checkCSNVersion } = require('../json/csnVersion'); | ||
const { getUtils } = require('../model/csnUtils'); | ||
const { getUtils, forEachDefinition } = require('../model/csnUtils'); | ||
const { optionProcessor } = require('../optionProcessor'); | ||
const { isBetaEnabled } = require('../base/model'); | ||
const { transformForHanaWithCsn } = require('../transform/forHanaNew'); | ||
// FIXME: This is not up-to-date in regards to the changes to hdbcds/sql quoting etc. | ||
/** | ||
* FIXME: Not yet supported, only in beta mode | ||
* FIXME: This is not up-to-date in regards to the changes to hdbcds/sql quoting etc. | ||
* | ||
* Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their | ||
* Generate SQL DDL rename statements for a migration, renaming existing tables and their | ||
* columns so that they match the result of "toHana" or "toSql" with the 'plain' option for names. | ||
* Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default). | ||
* The following options control what is actually generated: | ||
* The following options control what is actually generated (see help above): | ||
* options : { | ||
* toRename.names : existing names, either 'quoted' or 'hdbcds' (default) | ||
* sqlMapping : existing names, either 'quoted' or 'hdbcds' (default) | ||
* } | ||
@@ -25,30 +29,48 @@ * Return a dictionary of top-level artifacts by their names, like this: | ||
* @todo clarify input parameters | ||
* @param {CSN.Model} csn Augmented csn? | ||
* @param {CSN.Model} inputCsn CSN? | ||
* @param {CSN.Options} options Transformation options | ||
* @returns {object} A dictionary of name: rename statement | ||
*/ | ||
function toRenameDdl(csn, options) { | ||
// Merge options (arguments first, then model options and default) | ||
function toRename(inputCsn, options) { | ||
const { error, warning, throwWithError } = makeMessageFunction(inputCsn, options, 'to.rename'); | ||
// Merge options with defaults. | ||
options = Object.assign({ sqlMapping: 'hdbcds' }, options); | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toRename').forEach(complaint => warning(null, null, `${complaint}`)); | ||
checkCSNVersion(inputCsn, options); | ||
// Requires beta mode | ||
if (!isBetaEnabled(options, 'toRename')) | ||
error(null, null, 'Generation of SQL rename statements is not supported yet (only in beta mode)'); | ||
// FIXME: Currently, 'toRename' implies transformation for HANA (transferring the options to forHana) | ||
const csn = transformForHanaWithCsn(inputCsn, options, 'to.rename'); | ||
// forHanaCsn looses empty contexts and services, add them again so that toRename can calculate the namespaces | ||
forEachDefinition(csn, (artifact, artifactName) => { | ||
if ((artifact.kind === 'context' || artifact.kind === 'service') && csn.definitions[artifactName] === undefined) | ||
csn.definitions[artifactName] = artifact; | ||
}); | ||
const result = Object.create(null); | ||
const { getNamespaceOfArtifact } = getUtils(csn, false); | ||
checkCSNVersion(csn, options); | ||
const { getNamespaceOfArtifact } = getUtils(csn); | ||
// Render each artifact on its own | ||
for (const artifactName in csn.definitions) { | ||
const sourceStr = renameTableAndColumns(artifactName, csn.definitions[artifactName]); | ||
if (sourceStr !== '') | ||
result[artifactName] = sourceStr; | ||
} | ||
// Throw exception in case of errors | ||
if (hasErrors(options.messages)) | ||
throw new CompilationError(options.messages); | ||
return result; | ||
throwWithError(); | ||
return { | ||
rename: result, | ||
options, | ||
}; | ||
/** | ||
* If 'art' is a non-view entity, generate SQL statements to rename the corresponding | ||
* table and its columns from the naming conventions given in 'options.toRename.name' | ||
* table and its columns from the naming conventions given in 'options.sqlMapping' | ||
* (either 'quoted' or 'hdbcds') to 'plain'. In addition, drop any existing associations | ||
@@ -101,3 +123,3 @@ * from the columns (they would likely become invalid anyway). | ||
function absoluteCdsName(name) { | ||
if (options.toRename.names !== 'hdbcds') | ||
if (options.sqlMapping !== 'hdbcds') | ||
return name; | ||
@@ -113,3 +135,3 @@ | ||
/** | ||
* Return 'name' with appropriate "-quotes, also replacing '::' by '.' if 'options.toRename.names' | ||
* Return 'name' with appropriate "-quotes, also replacing '::' by '.' if 'options.sqlMapping' | ||
* is 'quoted' | ||
@@ -121,3 +143,3 @@ * | ||
function quoteSqlId(name) { | ||
if (options.toRename.names === 'quoted') | ||
if (options.sqlMapping === 'quoted') | ||
name = name.replace(/::/g, '.'); | ||
@@ -141,3 +163,3 @@ | ||
module.exports = { | ||
toRenameDdl, | ||
toRename, | ||
}; |
@@ -282,2 +282,14 @@ // Common render functions for toCdl.js, toHdbcds.js and toSql.js | ||
}, | ||
postgres: { | ||
// TODO: Type mapping for binary types is not correct, yet. | ||
// We can't use text types for binary on PostgreSQL due to NUL! | ||
'cds.String': 'VARCHAR', | ||
'cds.LargeString': 'text', | ||
'cds.hana.CLOB': 'text', | ||
'cds.LargeBinary': 'bytea', | ||
'cds.Binary': 'VARCHAR', | ||
'cds.hana.BINARY': 'VARCHAR', | ||
'cds.Double': 'double precision', | ||
'cds.hana.TINYINT': 'INTEGER', | ||
}, | ||
}; | ||
@@ -379,3 +391,3 @@ | ||
* @callback renderPart | ||
* @param {object||array} expression | ||
* @param {object|array} expression | ||
* @param {CdlRenderEnvironment} env | ||
@@ -402,2 +414,4 @@ * @this {{inline: Boolean, nestedExpr: Boolean}} | ||
* @property {renderPart} SET | ||
* @property {boolean} [inline] | ||
* @property {boolean} [nestedExpr] | ||
*/ | ||
@@ -404,0 +418,0 @@ |
@@ -43,14 +43,14 @@ // Render functions for toSql.js | ||
const renderAsHdbconstraint = options.transformation === 'hdbcds' || | ||
(options.toSql && options.toSql.src === 'hdi') || | ||
options.src === 'hdi' || | ||
(options.manageConstraints && options.manageConstraints.src === 'hdi'); | ||
const { names } = options.forHana; | ||
const forSqlite = options.toSql && options.toSql.dialect === 'sqlite'; | ||
const { sqlMapping } = options; | ||
const forSqlite = options.sqlDialect === 'sqlite'; | ||
let result = ''; | ||
result += `${indent}CONSTRAINT ${quoteId(constraint.identifier)}\n`; | ||
if (renderAsHdbconstraint) | ||
result += `${indent}ON ${quoteId(getResultingName(csn, names, constraint.dependentTable))}\n`; | ||
result += `${indent}ON ${quoteId(getResultingName(csn, sqlMapping, constraint.dependentTable))}\n`; | ||
if (!alterConstraint) { | ||
result += `${indent}FOREIGN KEY(${constraint.foreignKey.map(quoteId).join(', ')})\n`; | ||
result += `${indent}REFERENCES ${quoteId(getResultingName(csn, names, constraint.parentTable))}(${constraint.parentKey.map(quoteId).join(', ')})\n`; | ||
result += `${indent}REFERENCES ${quoteId(getResultingName(csn, sqlMapping, constraint.parentTable))}(${constraint.parentKey.map(quoteId).join(', ')})\n`; | ||
const onDeleteRemark = constraint.onDeleteRemark ? ` -- ${constraint.onDeleteRemark}` : ''; | ||
@@ -69,3 +69,4 @@ | ||
// constraint enforcement / validation must be switched off using sqlite pragma statement | ||
if (options.toSql && options.toSql.dialect !== 'sqlite') { | ||
// Does not include HDBCDS. | ||
if (options.toSql && !forSqlite) { | ||
result += `${indent}${!constraint.validated ? 'NOT ' : ''}VALIDATED\n`; | ||
@@ -75,3 +76,3 @@ result += `${indent}${!constraint.enforced ? 'NOT ' : ''}ENFORCED\n`; | ||
// for sqlite, the DEFERRABLE keyword is required | ||
result += `${indent}${options.toSql && options.toSql.dialect === 'sqlite' ? 'DEFERRABLE ' : ''}INITIALLY DEFERRED`; | ||
result += `${indent}${forSqlite ? 'DEFERRABLE ' : ''}INITIALLY DEFERRED`; | ||
return result; | ||
@@ -91,5 +92,5 @@ } | ||
* Additionally perform the following conversions on 'name' | ||
* If 'options.toSql.names' is 'plain' | ||
* If 'options.sqlMapping' is 'plain' | ||
* - replace '.' or '::' by '_' | ||
* else if 'options.toSql.names' is 'quoted' | ||
* else if 'options.sqlMapping' is 'quoted' | ||
* - replace '::' by '.' | ||
@@ -104,9 +105,9 @@ * Complain about names that collide with known SQL keywords or functions | ||
switch (options.toSql.names) { | ||
switch (options.sqlMapping) { | ||
case 'plain': | ||
return smartId(name, options.toSql.dialect); | ||
return smartId(name, options.sqlDialect); | ||
case 'quoted': | ||
return delimitedId(name, options.toSql.dialect); | ||
return delimitedId(name, options.sqlDialect); | ||
case 'hdbcds': | ||
return delimitedId(name, options.toSql.dialect); | ||
return delimitedId(name, options.sqlDialect); | ||
default: | ||
@@ -119,5 +120,5 @@ return undefined; | ||
* Prepare an identifier: | ||
* If 'options.toSql.names' is 'plain' | ||
* If 'options.sqlMapping' is 'plain' | ||
* - replace '.' or '::' by '_' | ||
* else if 'options.toSql.names' is 'quoted' | ||
* else if 'options.sqlMapping' is 'quoted' | ||
* - replace '::' by '.' | ||
@@ -130,7 +131,7 @@ * | ||
// Sanity check | ||
if (options.toSql.dialect === 'sqlite' && options.toSql.names !== 'plain') | ||
throw new ModelError(`Not expecting ${options.toSql.names} names for 'sqlite' dialect`); | ||
if (options.sqlDialect === 'sqlite' && options.sqlMapping !== 'plain') | ||
throw new ModelError(`Not expecting ${options.sqlMapping} names for 'sqlite' dialect`); | ||
switch (options.toSql.names) { | ||
switch (options.sqlMapping) { | ||
case 'plain': | ||
@@ -143,3 +144,3 @@ return name.replace(/(\.|::)/g, '_'); | ||
default: | ||
throw new ModelError(`No matching rendering found for naming mode ${options.toSql.names}`); | ||
throw new ModelError(`No matching rendering found for naming mode ${options.sqlMapping}`); | ||
} | ||
@@ -146,0 +147,0 @@ } |
@@ -48,2 +48,8 @@ // API functions returning the SQL identifier token text for a name | ||
}, | ||
postgres: { | ||
regularRegex: /^[A-Za-z_][A-Za-z_$0-9]*$/, | ||
reservedWords: keywords.postgres, | ||
effectiveName: name => name.toLowerCase(), | ||
asDelimitedId: name => `"${ name.replace(/"/g, '""')}"`, | ||
}, | ||
hana: { | ||
@@ -50,0 +56,0 @@ regularRegex: /^[A-Za-z_][A-Za-z_$#0-9]*$/, |
@@ -20,2 +20,3 @@ { | ||
"sonarjs/cognitive-complexity": "off", | ||
"sonarjs/no-duplicate-string": "off", | ||
// Does not recognize TS types | ||
@@ -25,7 +26,7 @@ "jsdoc/no-undefined-types": "off" | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"ecmaVersion": 2020, | ||
"sourceType": "script" | ||
}, | ||
"env": { | ||
"es6": true, | ||
"es2020": true, | ||
"node": true | ||
@@ -32,0 +33,0 @@ }, |
@@ -84,6 +84,8 @@ 'use strict'; | ||
function ignore(member, memberName, prop, path) { | ||
if (options.sqlDialect === 'hana' && !member._ignore && member.target && isAssocOrComposition(member.type) && isUnreachableAssociationTarget(csn.definitions[member.target])) { | ||
const targetAnnotation = hasAnnotationValue(csn.definitions[member.target], exists) ? exists : '@cds.persistence.skip'; | ||
if (options.sqlDialect === 'hana' && | ||
!member._ignore && member.target && | ||
isAssocOrComposition(member.type) && | ||
!isPersistedOnDatabase(csn.definitions[member.target])) { | ||
info(null, path, | ||
{ target: member.target, anno: targetAnnotation }, | ||
{ target: member.target, anno: '@cds.persistence.skip' }, | ||
'Association has been removed as it\'s target $(TARGET) is annotated with $(ANNO)'); | ||
@@ -93,14 +95,2 @@ member._ignore = true; | ||
} | ||
/** | ||
* Check whether the given artifact is an unreachable association target because it will not "realy" hit the database: | ||
* - @cds.persistence.skip/exists | ||
* - abstract | ||
* | ||
* @param {CSN.Artifact} art | ||
* @returns {boolean} | ||
*/ | ||
function isUnreachableAssociationTarget(art) { | ||
return !isPersistedOnDatabase(art) || hasAnnotationValue(art, exists); | ||
} | ||
} | ||
@@ -107,0 +97,0 @@ |
@@ -520,3 +520,3 @@ 'use strict'; | ||
// constraint identifier start with `c__` to avoid name clashes | ||
identifier: `c__${getResultingName(csn, options.forHana.names, artifactName)}_${$foreignKeyConstraint.sourceAssociation}`, | ||
identifier: `c__${getResultingName(csn, options.sqlMapping, artifactName)}_${$foreignKeyConstraint.sourceAssociation}`, | ||
foreignKey: dependentKey, | ||
@@ -523,0 +523,0 @@ parentKey, |
@@ -27,6 +27,7 @@ 'use strict'; | ||
function expandStructureReferences(csn, options, pathDelimiter, { error, info, throwWithAnyError }, iterateOptions = {}) { | ||
const csnUtils = getUtils(csn); | ||
const { | ||
isStructured, get$combined, getFinalBaseType, getServiceName, | ||
} = getUtils(csn); | ||
let { effectiveType, inspectRef } = csnRefs(csn); | ||
isStructured, get$combined, getFinalBaseTypeWithProps, getServiceName, | ||
} = csnUtils; | ||
let { effectiveType, inspectRef } = csnUtils; | ||
@@ -243,9 +244,9 @@ if (isBetaEnabled(options, 'nestedProjections')) | ||
if (parent.ref) { | ||
const finalBaseType = getFinalBaseType(parent._art.type); | ||
const finalBaseType = getFinalBaseTypeWithProps(parent._art.type); | ||
const art = parent._art; | ||
if (finalBaseType === 'cds.Association' || finalBaseType === 'cds.Composition') | ||
if (finalBaseType && (finalBaseType.type === 'cds.Association' || finalBaseType.type === 'cds.Composition')) | ||
return csn.definitions[art.target].elements; | ||
return art.elements || finalBaseType.elements; | ||
return art.elements || finalBaseType?.elements; | ||
} | ||
@@ -252,0 +253,0 @@ |
@@ -50,2 +50,3 @@ 'use strict'; | ||
function resolveTypeReferences(csn, options, resolved, pathDelimiter, iterateOptions = {}) { | ||
const typeCache = Object.create(null); // TODO: Argument as well? | ||
/** | ||
@@ -71,4 +72,4 @@ * Remove .localized from the element and any sub-elements | ||
} | ||
const { toFinalBaseType } = transformUtils.getTransformers(csn, options, pathDelimiter); | ||
const { getServiceName, getFinalBaseType } = getUtils(csn); | ||
const { toFinalBaseType, csnUtils } = transformUtils.getTransformers(csn, options, pathDelimiter); | ||
const { getServiceName, getFinalBaseTypeWithProps } = csnUtils; | ||
@@ -87,3 +88,3 @@ // We don't want to iterate over actions | ||
if (cast.type && !isBuiltinType(cast.type) && (!options.toOdata || options.toOdata && !isODataV4BuiltinFromService(cast.type, path) && !isODataItems(cast.type))) | ||
toFinalBaseType(parent.cast, resolved, true); | ||
toFinalBaseType(parent.cast, resolved, true, typeCache); | ||
}, | ||
@@ -95,3 +96,3 @@ // @ts-ignore | ||
if (!isBuiltinType(type) && (!options.toOdata || options.toOdata && !isODataV4BuiltinFromService(type, path) && !isODataItems(type))) { | ||
toFinalBaseType(parent, resolved); | ||
toFinalBaseType(parent, resolved, true, typeCache); | ||
// structured types might not have the child-types replaced. | ||
@@ -105,3 +106,3 @@ // Drill down to ensure this. | ||
if (e.type && !isBuiltinType(e.type)) | ||
toFinalBaseType(e, resolved); | ||
toFinalBaseType(e, resolved, true, typeCache); | ||
@@ -160,7 +161,7 @@ if (e.elements) | ||
function isODataV4BuiltinFromService(typeName, path) { | ||
if (!options.toOdata || (options.toOdata && options.toOdata.version === 'v2') || typeof typeName !== 'string') | ||
if (!options.toOdata || (options.odataVersion === 'v2') || typeof typeName !== 'string') | ||
return false; | ||
const typeServiceName = getServiceName(typeName); | ||
const finalBaseType = getFinalBaseType(typeName); | ||
const finalBaseType = getFinalBaseTypeWithProps(typeName)?.type; | ||
// we need the service of the current definition | ||
@@ -275,5 +276,4 @@ const currDefServiceName = getServiceName(path[1]); | ||
function flattenElements(csn, options, pathDelimiter, error, iterateOptions = {}) { | ||
const { isAssocOrComposition } = getUtils(csn); | ||
const { flattenStructuredElement } = transformUtils.getTransformers(csn, options, pathDelimiter); | ||
const { effectiveType } = csnRefs(csn); | ||
const { flattenStructuredElement, csnUtils } = transformUtils.getTransformers(csn, options, pathDelimiter); | ||
const { isAssocOrComposition, effectiveType } = csnUtils; | ||
const transformers = { | ||
@@ -309,7 +309,9 @@ elements: flatten, | ||
for (const flatElemName in flatElems) { | ||
if (parent[prop][flatElemName]) | ||
if (parent[prop][flatElemName]) { | ||
// TODO: combine message ID with generated FK duplicate | ||
// do the duplicate check in the consruct callback, requires to mark generated flat elements, | ||
// do the duplicate check in the construct callback, requires to mark generated flat elements, | ||
// check: Error location should be the existing element like @odata.foreignKey4 | ||
error(null, path.concat([ 'elements', elementName ]), `"${path[1]}.${elementName}": Flattened struct element name conflicts with existing element: "${flatElemName}"`); | ||
error('name-duplicate-element', path.concat([ 'elements', elementName ]), | ||
{ '#': 'flatten-element-exist', name: flatElemName }); | ||
} | ||
@@ -620,4 +622,3 @@ const flatElement = flatElems[flatElemName]; | ||
// error location is the colliding element | ||
error(null, eltPath, { name: fk[0], art: elementName }, | ||
'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element'); | ||
error('name-duplicate-element', eltPath, { '#': 'flatten-fkey-exists', name: fk[0], art: elementName }); | ||
} | ||
@@ -631,6 +632,4 @@ // attach a proper $path | ||
Object.entries(refCount).forEach(([ name, occ ]) => { | ||
if (occ > 1) { | ||
error(null, eltPath, { name }, | ||
'Duplicate definition of foreign key element $(NAME)'); | ||
} | ||
if (occ > 1) | ||
error('name-duplicate-element', eltPath, { '#': 'flatten-fkey-gen', name, art: elementName }); | ||
}); | ||
@@ -637,0 +636,0 @@ if (element.keys) { |
@@ -6,3 +6,3 @@ 'use strict'; | ||
} = require('../../model/csnUtils'); | ||
const { implicitAs, csnRefs } = require('../../model/csnRefs'); | ||
const { implicitAs } = require('../../model/csnRefs'); | ||
const { isBetaEnabled } = require('../../base/model'); | ||
@@ -73,5 +73,5 @@ const { ModelError } = require('../../base/error'); | ||
get$combined, isAssocOrComposition, | ||
inspectRef, queryOrMain, // csnRefs | ||
} = getUtils(csn); | ||
const { inspectRef, queryOrMain } = csnRefs(csn); | ||
const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_'; | ||
const pathDelimiter = options.forHana && (options.sqlMapping === 'hdbcds') ? '.' : '_'; | ||
const { error, info } = messageFunctions; | ||
@@ -78,0 +78,0 @@ const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds'); |
@@ -26,7 +26,7 @@ { | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"ecmaVersion": 2020, | ||
"sourceType": "script" | ||
}, | ||
"env": { | ||
"es6": true, | ||
"es2020": true, | ||
"node": true | ||
@@ -33,0 +33,0 @@ }, |
'use strict'; | ||
const { | ||
hasAnnotationValue, getUtils, getServiceNames, forEachDefinition, | ||
hasAnnotationValue, getServiceNames, forEachDefinition, | ||
getResultingName, forEachMemberRecursively, | ||
@@ -22,3 +22,3 @@ } = require('../../model/csnUtils'); | ||
function generateDrafts(csn, options, pathDelimiter, messageFunctions) { | ||
const draftSuffix = isDeprecatedEnabled(options, 'generatedEntityNameWithUnderscore') ? '_drafts' : '.drafts'; | ||
const draftSuffix = isDeprecatedEnabled(options, '_generatedEntityNameWithUnderscore') ? '_drafts' : '.drafts'; | ||
// All services of the model - needed for drafts | ||
@@ -29,5 +29,5 @@ const allServices = getServiceNames(csn); | ||
createForeignKeyElement, createAndAddDraftAdminDataProjection, createScalarElement, createAssociationElement, | ||
addElement, copyAndAddElement, createAssociationPathComparison, | ||
addElement, copyAndAddElement, createAssociationPathComparison, csnUtils, | ||
} = getTransformers(csn, options, pathDelimiter); | ||
const { getCsnDef, isComposition } = getUtils(csn); | ||
const { getCsnDef, isComposition } = csnUtils; | ||
const { error, warning } = messageFunctions; | ||
@@ -160,3 +160,3 @@ | ||
const persistenceName = getResultingName(csn, options.forHana.names, draftsArtifactName); | ||
const persistenceName = getResultingName(csn, options.sqlMapping, draftsArtifactName); | ||
// Duplicate the artifact as a draft shadow entity | ||
@@ -193,3 +193,3 @@ if (csn.definitions[persistenceName]) { | ||
let elem; | ||
if ((isDeprecatedEnabled(options, 'renderVirtualElements') && origElem.virtual) || !origElem.virtual) | ||
if ((isDeprecatedEnabled(options, '_renderVirtualElements') && origElem.virtual) || !origElem.virtual) | ||
elem = copyAndAddElement(origElem, draftsArtifact, draftsArtifactName, elemName)[elemName]; | ||
@@ -196,0 +196,0 @@ if (elem) { |
'use strict'; | ||
const { forEachDefinition, getUtils, getServiceNames } = require('../../model/csnUtils'); | ||
const { forEachDefinition, getServiceNames } = require('../../model/csnUtils'); | ||
const { forEach } = require('../../utils/objectUtils'); | ||
@@ -33,4 +33,4 @@ const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils'); | ||
resetAnnotation, | ||
csnUtils, | ||
} = getTransformers(csn, options); | ||
const { | ||
@@ -40,5 +40,4 @@ getFinalType, | ||
hasAnnotationValue, | ||
getFinalBaseType, | ||
getFinalTypeDef, | ||
} = getUtils(csn); | ||
getFinalBaseTypeWithProps, | ||
} = csnUtils; | ||
@@ -202,4 +201,4 @@ const { error, info } = makeMessageFunction(csn, options, 'for.odata'); | ||
else if (elem.type) { // types - possibly structured | ||
const typeDef = elem.type.ref ? getFinalBaseType(elem.type) : getFinalTypeDef(elem.type); | ||
if (typeDef.elements) | ||
const typeDef = getFinalBaseTypeWithProps(elem.type); | ||
if (typeDef?.elements) | ||
stack.push(typeDef); | ||
@@ -206,0 +205,0 @@ } |
@@ -44,5 +44,3 @@ 'use strict'; | ||
* options = { | ||
* forHana.names // See the behavior of 'names' in toHana, toSql and toRename | ||
* forHana.alwaysResolveDerivedTypes // Always resolve derived type chains (by default, this is only | ||
* // done for 'quoted' names). FIXME: Should always be done in general. | ||
* sqlMapping // See the behavior of 'sqlMapping' in toHana, toSql and toRename | ||
* } | ||
@@ -73,5 +71,5 @@ * The result model will always have 'options.forHana' set, to indicate that these transformations have happened. | ||
* - (130) (not for to.hdbcds with hdbcds names): Elements having structured types are flattened into | ||
* multiple elements (using '_' or '.' as name separator, depending on 'forHana.names'). | ||
* multiple elements (using '_' or '.' as name separator, depending on 'sqlMapping'). | ||
* - (140) (not for to.hdbcds with hdbcds names): Managed associations get explicit ON-conditions, with | ||
* generated foreign key elements (also using '_' or '.' as name separator, depending on 'forHana.names'). | ||
* generated foreign key elements (also using '_' or '.' as name separator, depending on 'sqlMapping'). | ||
* - (150) (a) Elements from inherited (included) entities are copied into the receiving entity | ||
@@ -99,3 +97,3 @@ * (b) The 'include' property is removed from entities. | ||
* with their database name (as '@cds.persistence.name') according to the naming convention chosen | ||
* in 'options.forHana.names'. | ||
* in 'options.sqlMapping'. | ||
* - (250) Remove name space definitions again (only in forHanaNew). Maybe we can omit inserting namespace definitions | ||
@@ -114,7 +112,5 @@ * completely (TODO) | ||
checkCSNVersion(csn, options); | ||
const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_'; | ||
const pathDelimiter = (options.sqlMapping === 'hdbcds') ? '.' : '_'; | ||
@@ -125,3 +121,2 @@ let message, error, warning, info; // message functions | ||
let artifactRef, inspectRef, effectiveType, get$combined, | ||
getFinalBaseType, // csnUtils (csnRefs) | ||
addDefaultTypeFacets, expandStructsInExpression; // transformUtils | ||
@@ -138,3 +133,3 @@ | ||
const dialect = options.forHana && options.forHana.dialect || options.toSql && options.toSql.dialect; | ||
const dialect = options.sqlDialect; | ||
const doA2J = !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds'); | ||
@@ -146,3 +141,3 @@ if (!doA2J) | ||
const cleanup = validate.forHana(csn, { | ||
message, error, warning, info, inspectRef, effectiveType, artifactRef, csnUtils: getUtils(csn), csn, options, getFinalBaseType, isAspect | ||
message, error, warning, info, inspectRef, effectiveType, artifactRef, csnUtils: getUtils(csn), csn, options, isAspect | ||
}); | ||
@@ -232,2 +227,3 @@ | ||
isAssociationOperand, isDollarSelfOrProjectionOperand, | ||
csnUtils, | ||
} = transformUtils.getTransformers(csn, options, pathDelimiter); | ||
@@ -240,3 +236,3 @@ | ||
cloneWithTransformations, | ||
} = getUtils(csn); | ||
} = csnUtils; | ||
@@ -421,4 +417,5 @@ // (000) Rename primitive types, make UUID a String | ||
({ error, warning, info, message, throwWithAnyError } = makeMessageFunction(csn, options, moduleName)); | ||
({ artifactRef, inspectRef, effectiveType, getFinalBaseType, get$combined } = getUtils(csn)); | ||
({ addDefaultTypeFacets, expandStructsInExpression } = transformUtils.getTransformers(csn, options, pathDelimiter)); | ||
// TODO: Can we use csnUtils of the call above (transformUtils.getTransformers)? | ||
({ artifactRef, inspectRef, effectiveType, get$combined } = getUtils(csn)); | ||
} | ||
@@ -522,3 +519,3 @@ | ||
if (artifact.kind !== 'service' && artifact.kind !== 'context') | ||
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.forHana.names, csn), artifact); | ||
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact); | ||
@@ -531,3 +528,3 @@ forEachMemberRecursively(artifact, (member, memberName, property, path) => { | ||
if ((!member.virtual || artifact.query)) | ||
addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.forHana.names), member); | ||
addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.sqlMapping, options.sqlDialect), member); | ||
}, [ 'definitions', artifactName ]); | ||
@@ -558,3 +555,3 @@ } | ||
// mixin elements must be transformed, why can't toSql also use mixins? | ||
if(artifact.kind === 'entity' || artifact.query || (options.toHana && options.toHana.names === 'hdbcds' && artifact.kind == 'type')) | ||
if(artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type')) | ||
doit(artifact.elements, path.concat([ 'elements' ])); | ||
@@ -590,3 +587,3 @@ if (artifact.query && artifact.query.SELECT && artifact.query.SELECT.mixin) | ||
// - association that points to entity with parameters | ||
if (options.forHana.dialect === 'hana' && member.target && isAssocOrComposition(member.type) && !isBetaEnabled(options, 'assocsWithParams')) { | ||
if (options.sqlDialect === 'hana' && member.target && isAssocOrComposition(member.type) && !isBetaEnabled(options, 'assocsWithParams')) { | ||
if (artifact.params) { | ||
@@ -634,3 +631,3 @@ // HANA does not allow 'WITH ASSOCIATIONS' on something with parameters: | ||
} | ||
else if (options.forHana.dialect === 'sqlite') { // view with params | ||
else if (options.sqlDialect === 'sqlite') { // view with params | ||
// Allow with plain | ||
@@ -644,4 +641,4 @@ error(null, [ 'definitions', artifactName ], `SQLite does not support entities with parameters`); | ||
} | ||
else if (options.forHana.names !== 'plain' && pname.toUpperCase() !== pname) { // not plain mode: param name must be all upper | ||
warning(null, [ 'definitions', artifactName, 'params', pname ], { name: options.forHana.names }, | ||
else if (options.sqlMapping !== 'plain' && pname.toUpperCase() !== pname) { // not plain mode: param name must be all upper | ||
warning(null, [ 'definitions', artifactName, 'params', pname ], { name: options.sqlMapping }, | ||
'Expecting parameter to be uppercase in naming mode $(NAME)'); | ||
@@ -1067,3 +1064,3 @@ } | ||
function isMaxParameterLengthRestricted(type) { | ||
return !(options.toSql && type === 'cds.String' && (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain')); | ||
return !(options.toSql && type === 'cds.String' && (options.sqlDialect === 'sqlite' || options.sqlDialect === 'plain')); | ||
} | ||
@@ -1070,0 +1067,0 @@ |
@@ -6,4 +6,3 @@ 'use strict'; | ||
const transformUtils = require('./transformUtilsNew'); | ||
const { getUtils, | ||
cloneCsnNonDict, | ||
const { cloneCsnNonDict, | ||
forEachDefinition, | ||
@@ -90,6 +89,6 @@ forEachMemberRecursively, | ||
recurseElements, setAnnotation, renameAnnotation, | ||
expandStructsInExpression | ||
expandStructsInExpression, | ||
csnUtils, | ||
} = transformers; | ||
const csnUtils = getUtils(csn); | ||
const { | ||
@@ -103,7 +102,6 @@ getCsnDef, | ||
effectiveType, | ||
getFinalBaseType, | ||
} = csnUtils; | ||
// are we working with structured OData or not | ||
const structuredOData = options.toOdata.odataFormat === 'structured' && options.toOdata.version === 'v4'; | ||
const structuredOData = options.odataFormat === 'structured' && options.odataVersion === 'v4'; | ||
@@ -121,3 +119,3 @@ // collect all declared non-abstract services from the model | ||
const keepLocalizedViews = isDeprecatedEnabled(options, 'createLocalizedViews'); | ||
const keepLocalizedViews = isDeprecatedEnabled(options, '_createLocalizedViews'); | ||
@@ -132,3 +130,3 @@ function acceptLocalizedView(_name, parent) { | ||
const cleanup = validate.forOdata(csn, { | ||
message, error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, getFinalBaseType, isAspect, isExternalServiceMember | ||
message, error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, isAspect, isExternalServiceMember | ||
}); | ||
@@ -222,4 +220,4 @@ | ||
// Skip artifacts that have no DB equivalent anyway | ||
if (options.toOdata.names && !(def.kind in skipPersNameKinds)) | ||
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.toOdata.names, csn); | ||
if (options.sqlMapping && !(def.kind in skipPersNameKinds)) | ||
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.sqlMapping, csn, 'hana'); // hana to allow naming mode "hdbcds" | ||
@@ -230,5 +228,5 @@ forEachMemberRecursively(def, (member, memberName, propertyName) => { | ||
// as they have no DB representation (although in views) | ||
if (options.toOdata.names && typeof member === 'object' && !(member.kind === 'action' || member.kind === 'function') && propertyName !== 'enum' && (!member.virtual || def.query)) { | ||
if (options.sqlMapping && typeof member === 'object' && !(member.kind === 'action' || member.kind === 'function') && propertyName !== 'enum' && (!member.virtual || def.query)) { | ||
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation | ||
member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.toOdata.names); | ||
member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds" | ||
} | ||
@@ -235,0 +233,0 @@ |
'use strict'; | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { setProp } = require('../base/model'); | ||
const { setProp, isDeprecatedEnabled} = require('../base/model'); | ||
const { hasErrors } = require('../base/messages'); | ||
const { cloneCsnDictionary, applyDefinitionAnnotationsFromExtensions} = require('../model/csnUtils'); | ||
const { forEachKey } = require('../utils/objectUtils'); | ||
const { cleanSymbols } = require('../base/cleanSymbols.js'); | ||
const { | ||
cloneCsnDictionary, | ||
cloneCsnNonDict, | ||
applyAnnotationsFromExtensions, | ||
forEachDefinition, | ||
@@ -70,3 +72,4 @@ forEachGeneric, | ||
* join in direct convenience views. | ||
* @param {acceptLocalizedView} [acceptLocalizedView] optional callback function returning true if the localized view name and its parent name provided as parameter should be created | ||
* @param {acceptLocalizedView} [acceptLocalizedView] optional callback function returning true if the localized view | ||
* name and its parent name provided as parameter should be created | ||
*/ | ||
@@ -91,3 +94,3 @@ function _addLocalizationViews(csn, options, useJoins, acceptLocalizedView = null) { | ||
// In case that the user tried to annotate `localized.*` artifacts, apply them. | ||
applyDefinitionAnnotationsFromExtensions(csn, { | ||
applyAnnotationsFromExtensions(csn, { | ||
overwrite: true, | ||
@@ -152,3 +155,3 @@ filter: (name) => name.startsWith('localized.') | ||
copyPersistenceAnnotations(view, art); | ||
copyPersistenceAnnotations(view, art, options); | ||
csn.definitions[viewName] = view; | ||
@@ -673,17 +676,20 @@ } | ||
/** | ||
* Copy some @cds.persistence.* annotations from the source to | ||
* the target. Ignores existing annotations on the target. | ||
* Copy @cds.persistence.exists/skip annotations from the source to | ||
* the target. Ignores existing annotations on the _target_. | ||
* | ||
* @param {CSN.Artifact} target | ||
* @param {CSN.Artifact} source | ||
* @param {CSN.Options} options | ||
*/ | ||
function copyPersistenceAnnotations(target, source) { | ||
Object.keys(source) | ||
.forEach(anno => { | ||
// Do NOT copy ".exists" at the moment. ".exists" is not propagated | ||
// and this would lead to some localization views referencing not-existing | ||
// "localized.XYZ" views. | ||
if (anno === '@cds.persistence.skip') | ||
target[anno] = source[anno]; | ||
}); | ||
function copyPersistenceAnnotations(target, source, options) { | ||
const doNotCopyExists = isDeprecatedEnabled( options, 'eagerPersistenceForGeneratedEntities' ); | ||
forEachKey(source, anno => { | ||
// Note: | ||
// Because `.exists` is copied to the convenience view, it could | ||
// lead to some localization views referencing non-existing ones. | ||
// But that is the contract: User says that it already exists! | ||
// In v2, `.exists` was never copied. | ||
if (anno === '@cds.persistence.skip' || (!doNotCopyExists && anno === '@cds.persistence.exists')) | ||
target[anno] = source[anno]; | ||
}); | ||
} | ||
@@ -690,0 +696,0 @@ |
@@ -10,3 +10,3 @@ 'use strict'; | ||
function expandToFinalBaseType(csn, transformers, csnUtils, services, options, isExternalServiceMember) { | ||
const isV4 = options.toOdata.version === 'v4'; | ||
const isV4 = options.odataVersion === 'v4'; | ||
forEachDefinition(csn, (def, defName) => { | ||
@@ -91,5 +91,5 @@ // Unravel derived type chains to final one for elements, actions, action parameters (propagating annotations) | ||
// elements have precedence over type | ||
if (node.type && (!isBuiltinType(node.type) &&isExpandable(node, defName) || node.kind === 'type')) { | ||
if (node.type && (!isBuiltinType(node.type) && isExpandable(node, defName) || node.kind === 'type')) { | ||
// 1. Get the final type of the node (resolve derived type chain) | ||
const finalType = csnUtils.getFinalBaseType(node.type); | ||
const finalType = csnUtils.getFinalBaseTypeWithProps(node.type); | ||
if (finalType) { | ||
@@ -106,3 +106,3 @@ // The type replacement depends on whether 'node' is a definition or a member[element]. | ||
// type C { .... }; | ||
if (isBuiltinType(finalType)) { | ||
if (isBuiltinType(finalType.type)) { | ||
// use transformUrilsNew::toFinalBaseType for the moment, | ||
@@ -135,4 +135,4 @@ // as it is collects along the chain of types | ||
// type C { .... }; | ||
if (isBuiltinType(finalType)) { | ||
// use transformUrilsNew::toFinalBaseType for the moment, | ||
if (isBuiltinType(finalType.type)) { | ||
// use transformUtilsNew::toFinalBaseType for the moment, | ||
// as it is collects along the chain of types | ||
@@ -155,4 +155,4 @@ // attributes that need to be propagated | ||
const finalType = csnUtils.getFinalTypeDef(node.type); | ||
if (finalType.items && | ||
(isBuiltinType(finalType.items.type) || isBuiltinType(csnUtils.getFinalBaseType(finalType.items.type)))) | ||
if (finalType.items && | ||
(isBuiltinType(finalType.items.type) || isBuiltinType(csnUtils.getFinalBaseTypeWithProps(finalType.items.type)?.type))) | ||
{ | ||
@@ -194,4 +194,4 @@ if (!isArtifactInService(node.type, currService) || !isV4) { | ||
// a builtin from the service - do not expand to the final base type | ||
let finalBaseType = csnUtils.getFinalBaseType(node.type); | ||
// if (finalBaseType && finalBaseType.items) finalBaseType = csnUtils.getFinalBaseType(finalBaseType.items); | ||
let finalBaseType = csnUtils.getFinalBaseTypeWithProps(node.type).type; | ||
// if (finalBaseType && finalBaseType.items) finalBaseType = csnUtils.getFinalBaseTypeWithProps(finalBaseType.items); | ||
const currService = csnUtils.getServiceName(defName); | ||
@@ -198,0 +198,0 @@ return node.type && !node.type.ref |
@@ -17,3 +17,3 @@ 'use strict'; | ||
// are we working with OData proxies or cross-service refs | ||
const isMultiSchema = options.toOdata.version === 'v4' && (options.toOdata.odataProxies || options.toOdata.odataXServiceRefs); | ||
const isMultiSchema = options.odataVersion === 'v4' && (options.odataProxies || options.odataXServiceRefs); | ||
// collect in this variable all the newly exposed types | ||
@@ -126,3 +126,3 @@ const schemas = Object.create(null); | ||
defName = typeDef.kind === 'type' ? typeName : defName; | ||
exposeTypeOf(newElem, isKey, elemName, defName, serviceName, | ||
exposeTypeOf(newElem, isKey, elemName, defName, serviceName, | ||
getNewTypeName(newElem, elemName, newTypeName, serviceName), path, fullQualifiedNewTypeName); | ||
@@ -156,3 +156,3 @@ }); | ||
* check if this is a structured type | ||
* Returns an object that indicates | ||
* Returns an object that indicates | ||
* - wether or not the type needs exposure | ||
@@ -159,0 +159,0 @@ * - the elements dictionary that needs to be cloned |
@@ -1,37 +0,3 @@ | ||
const { | ||
forEachDefinition, | ||
forEachMemberRecursively, | ||
} = require('../../model/csnUtils'); | ||
'use strict'; | ||
// Return true if 'artifact' has an association type | ||
function isAssociation(artifact) { | ||
return (artifact.type === 'cds.Association' || artifact.type === 'Association'); | ||
} | ||
// Return true if 'artifact' has a composition type | ||
function isComposition(artifact) { | ||
return (artifact.type === 'cds.Composition' || artifact.type === 'Composition') | ||
} | ||
function isAssociationOrComposition(artifact) { | ||
return isAssociation(artifact) || isComposition(artifact); | ||
} | ||
function isManagedAssociation(artifact) { | ||
return artifact.target !== undefined && artifact.on === undefined; | ||
} | ||
function forEachManagedAssociation(csn, callback, isExternalServiceMember) { | ||
forEachDefinition(csn, (def) => { | ||
forEachMemberRecursively(def, (element) => { | ||
if (isAssociationOrComposition(element) && !element.on) { | ||
callback(element) | ||
} | ||
}) | ||
}, { skipArtifact: isExternalServiceMember }); | ||
} | ||
/** | ||
@@ -90,3 +56,2 @@ * Return the definition name, without the prefixed service name | ||
module.exports = { | ||
forEachManagedAssociation, | ||
defNameWithoutServiceOrContextName, | ||
@@ -96,5 +61,3 @@ getServiceOfArtifact, | ||
isArtifactInSomeService, | ||
isAssociationOrComposition, | ||
isLocalizedArtifactInService, | ||
isManagedAssociation, | ||
} |
@@ -9,6 +9,6 @@ 'use strict'; | ||
const { setProp } = require('../base/model'); | ||
const { csnRefs } = require('../model/csnRefs'); | ||
const { copyAnnotations, applyTransformations } = require('../model/csnUtils'); | ||
const { cloneCsnNonDict, cloneCsnDictionary, getUtils, isBuiltinType } = require('../model/csnUtils'); | ||
const { cloneCsnNonDict, cloneCsnDictionary, getUtils } = require('../model/csnUtils'); | ||
const { typeParameters, isBuiltinType } = require('../compiler/builtins'); | ||
const { ModelError } = require("../base/error"); | ||
@@ -24,16 +24,14 @@ const { forEach } = require('../utils/objectUtils'); | ||
const { error, warning, info } = makeMessageFunction(model, options); | ||
const csnUtils = getUtils(model); | ||
const { | ||
getCsnDef, | ||
getFinalBaseType, | ||
getFinalBaseTypeWithProps, | ||
hasAnnotationValue, | ||
inspectRef, | ||
isStructured, | ||
} = getUtils(model); | ||
const { | ||
effectiveType, | ||
} = csnRefs(model); | ||
} = csnUtils; | ||
return { | ||
csnUtils, | ||
resolvePath, | ||
@@ -50,3 +48,2 @@ flattenPath, | ||
isDollarSelfOrProjectionOperand, | ||
getFinalBaseType, | ||
createExposingProjection, | ||
@@ -249,3 +246,3 @@ createAndAddDraftAdminDataProjection, | ||
if (!elem.elements) // structures do not have final base type | ||
elemType = getFinalBaseType(elem.type); | ||
elemType = getFinalBaseTypeWithProps(elem.type); | ||
@@ -256,11 +253,9 @@ const struct = elemType ? elemType.elements : elem.elements; | ||
// TODO: Do not report collisions in the generated elements here, but instead | ||
// leave that work to the receiver of this result | ||
// leave that work to the receiver of this result | ||
let result = Object.create(null); | ||
const addGeneratedFlattenedElement = (e, eName) => { | ||
if(result[eName]){ | ||
error(null, pathInCsn, { name: eName }, | ||
'Generated element $(NAME) conflicts with other generated element') | ||
} else { | ||
if (result[eName]) | ||
error('name-duplicate-element', pathInCsn, { '#': 'flatten-element-gen', name: eName }) | ||
else | ||
result[eName] = e; | ||
} | ||
} | ||
@@ -295,3 +290,7 @@ forEach(struct, (childName, childElem) => { | ||
forEach(result, (name, flatElem) => { | ||
// Copy annotations from struct (not overwriting, because deep annotations should have precedence) | ||
// Copy annotations from struct (not overwriting, because deep annotations should have precedence). | ||
// Attention: | ||
// This has historic reasons. We don't copy doc-comments because copying annotations | ||
// is questionable to begin with. Only selected annotations should have been copied, | ||
// if at all. | ||
copyAnnotations(elem, flatElem, false); | ||
@@ -404,74 +403,61 @@ // Copy selected type properties | ||
/** | ||
* Replace the type of 'node' with its final base type (in contrast to the compiler, | ||
* also unravel derived enum types, i.e. take the final base type of the enum's base type. | ||
* Similar with associations and compositions (we probably need a _baseType link) | ||
* Replace the type of 'nodeWithType' with its final base type, i.e. copy relevant type properties and | ||
* set the `type` property to the builtin if scalar or delete it if structured/arrayed. | ||
* | ||
* @param {CSN.Artifact} node | ||
* @param {object} nodeWithType | ||
* @param {WeakMap} [resolved] WeakMap containing already resolved refs | ||
* @param {boolean} [keepLocalized=false] Whether to clone .localized from a type def | ||
* @returns {void} | ||
*/ | ||
function toFinalBaseType(node, resolved, keepLocalized=false) { | ||
// Nothing to do if no type (or if array/struct type) | ||
if (!node || !node.type) return; | ||
// In case of a ref -> Follow the ref | ||
if (node.type && node.type.ref) { | ||
const finalBaseType = getFinalBaseType(node.type, undefined, resolved); | ||
if(finalBaseType === null) | ||
throw Error('Failed to obtain final base type for reference : ' + node.type.ref.join('/')); | ||
if(finalBaseType.elements) { | ||
// This changes the order - to be discussed! | ||
node.elements = cloneCsnNonDict(finalBaseType, options).elements; // copy elements | ||
delete node.type; // delete the type reference as edm processing does not expect it | ||
} else if(finalBaseType.items) { | ||
// This changes the order - to be discussed! | ||
node.items = cloneCsnNonDict(finalBaseType.items, options); // copy items | ||
delete node.type; | ||
} else { | ||
node.type=finalBaseType; | ||
} | ||
function toFinalBaseType(nodeWithType, resolved = new WeakMap(), keepLocalized = false) { | ||
const type = nodeWithType?.type; | ||
if (!type || nodeWithType.elements || nodeWithType.items || resolved.has(nodeWithType)) { | ||
return; | ||
} | ||
// .. or builtin already | ||
if (node.type && isBuiltinType(node.type)) return; | ||
// The caller may use `{ art }` syntax for `{ ref }` objects, but we only use | ||
// it to indicate that an artifact has been processed. | ||
resolved.set(nodeWithType, nodeWithType); | ||
// The type might already be a full fledged type def (array of) | ||
let typeDef = typeof node.type === 'string' ? getCsnDef(node.type) : node.type; | ||
// Nothing to do if type is an array or a struct type | ||
if (typeDef.items || typeDef.elements) { | ||
// cloneCsn only works correctly if we start "from the top" | ||
const cloneTypeDef = cloneCsnNonDict(typeDef, options); | ||
// With hdbcds-hdbcds, don't resolve structured types - but propagate ".items", to turn into LargeString later on. | ||
if(typeDef.items) { | ||
delete node.type; | ||
if(!node.items) | ||
Object.assign(node, {items: cloneTypeDef.items}); | ||
// Nothing to copy from builtin. | ||
if (typeof type === 'string' && isBuiltinType(type)) | ||
return; | ||
let typeRef = null; | ||
if (resolved.has(type)) { | ||
typeRef = resolved.get(type)?.art | ||
// The cached entry may not be resolved, yet. | ||
if (typeRef.type && !isBuiltinType(typeRef.type)) | ||
typeRef = getFinalBaseTypeWithProps(typeRef.type); | ||
} else { | ||
typeRef = getFinalBaseTypeWithProps(type); | ||
} | ||
if (typeRef.elements || typeRef.items) { | ||
// Copy elements/items and we're finished. No need to look up actual base type, | ||
// since it must also be structured and must contain at least as many elements, | ||
// if not more (in client style CSN). | ||
if (typeRef.elements && !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds')) { | ||
nodeWithType.elements = cloneCsnDictionary(typeRef.elements, options); | ||
delete nodeWithType.type; | ||
} | ||
if(typeDef.elements && !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds')) { | ||
if(!typeDef.items) | ||
delete node.type; | ||
if(!node.elements) | ||
Object.assign(node, {elements: cloneTypeDef.elements}); | ||
if (typeRef.items) { | ||
nodeWithType.items = cloneCsnNonDict(typeRef.items, options); | ||
delete nodeWithType.type; | ||
} | ||
return; | ||
} | ||
if (typeRef.enum && nodeWithType.enum === undefined) | ||
nodeWithType.enum = cloneCsnDictionary(typeRef.enum, options); | ||
return; | ||
// Copy type and type arguments (+ localized) | ||
for (const param of typeParameters.list) { | ||
if (nodeWithType[param] === undefined && typeRef[param] !== undefined &&!typeRef.$default) { | ||
nodeWithType[param] = typeRef[param]; | ||
} | ||
} | ||
// if the declared element is an enum, these values are with priority | ||
if (!node.enum && typeDef.enum) { | ||
const clone = cloneCsnDictionary(typeDef.enum, options); | ||
Object.assign(node, { enum: clone }); | ||
} | ||
if (node.length === undefined && typeDef.length !== undefined) | ||
Object.assign(node, { length: typeDef.length }); | ||
if (node.precision === undefined && typeDef.precision !== undefined) | ||
Object.assign(node, { precision: typeDef.precision }); | ||
if (node.scale === undefined && typeDef.scale !== undefined) | ||
Object.assign(node, { scale: typeDef.scale }); | ||
if (node.srid === undefined && typeDef.srid !== undefined) | ||
Object.assign(node, { srid: typeDef.srid }); | ||
if (keepLocalized && node.localized === undefined && typeDef.localized !== undefined) | ||
Object.assign(node, { localized: typeDef.localized }); | ||
node.type = typeDef.type; | ||
toFinalBaseType(node); | ||
if (keepLocalized && nodeWithType.localized === undefined && typeRef.localized !== undefined) | ||
nodeWithType.localized = typeRef.localized; | ||
if (typeRef.type) | ||
nodeWithType.type = typeRef.type; | ||
} | ||
@@ -478,0 +464,0 @@ |
@@ -24,7 +24,7 @@ { | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"ecmaVersion": 2020, | ||
"sourceType": "script" | ||
}, | ||
"env": { | ||
"es6": true, | ||
"es2020": true, | ||
"node": true | ||
@@ -31,0 +31,0 @@ }, |
@@ -15,3 +15,3 @@ 'use strict'; | ||
const { | ||
artifactRef, getColumn, getElement, | ||
artifactRef, getColumn, getElement, getOrigin, | ||
} = getUtils(csn, 'init-all'); | ||
@@ -61,3 +61,11 @@ | ||
return column; | ||
return getElementFromFrom(name, base.from); | ||
const from = getElementFromFrom(name, base.from); | ||
if (from) | ||
return from; | ||
// For .expand/.inline, we can find it via origin | ||
// Although I would have expected to find it via getColumn... | ||
const origin = getOrigin(element); | ||
if (origin) | ||
return origin; | ||
throw new Error(`Could not find ancestor for ${JSON.stringify(element)} named ${name}`); | ||
} | ||
@@ -92,6 +100,3 @@ | ||
else if (base.args && base.join) { | ||
const result = checkJoinSources(base.args, name); | ||
if (!result) | ||
throw new Error(`Could not find ${name} in ${JSON.stringify(base.args)}`); | ||
return result; | ||
return checkJoinSources(base.args, name); | ||
} | ||
@@ -98,0 +103,0 @@ |
@@ -32,6 +32,3 @@ 'use strict'; | ||
'@fiori.draft.enabled': onlyViaArtifact, | ||
'@': (prop, target, source) => { | ||
if (source[prop] !== null) | ||
target[prop] = source[prop]; | ||
}, | ||
'@': nullStopsPropagation, | ||
// Example: `type E : F;` does not have `elements`, but they are required for e.g. OData. | ||
@@ -52,3 +49,3 @@ elements: onlyTypeDef, | ||
type: always, | ||
doc: always, | ||
doc: nullStopsPropagation, | ||
length: always, | ||
@@ -99,2 +96,6 @@ precision: always, | ||
val: always, | ||
type: notWithItemsOrElements, | ||
target: notWithItemsOrElements, | ||
keys: notWithItemsOrElements, | ||
cardinality: notWithItemsOrElements, | ||
}; | ||
@@ -709,2 +710,15 @@ | ||
/** | ||
* The value `null` tells us to skip the propagation of the property. | ||
* This is the case e.g. for `doc` or for annotations. | ||
* | ||
* @param {string} prop | ||
* @param {CSN.Element} target | ||
* @param {CSN.Element} source | ||
*/ | ||
function nullStopsPropagation(prop, target, source) { | ||
if (source[prop] !== null) | ||
target[prop] = source[prop]; | ||
} | ||
/** | ||
* Special propagation rules for .items - depending on the exact type of .items and the | ||
@@ -730,2 +744,16 @@ * way it was referenced (type of, direct type, direct many), we need to propagate (or not). | ||
/** | ||
* Don't propagate certain properties if the target already has a .items or .elements | ||
* | ||
* This happens with .expand/.inline | ||
* | ||
* @param {string} prop | ||
* @param {CSN.Element} target | ||
* @param {CSN.Element} source | ||
*/ | ||
function notWithItemsOrElements(prop, target, source) { | ||
if (!target.items && !target.elements || !source.target) | ||
target[prop] = source[prop]; | ||
} | ||
/** | ||
* Some properties must not be copied over if the type of this member | ||
@@ -732,0 +760,0 @@ * is a reference to another element. |
@@ -64,3 +64,3 @@ // Util functions for operations usually used with files. | ||
function realpath(path, cb) { | ||
return fs.realpath(path, cb); | ||
return fs.realpath.native(path, cb); | ||
} | ||
@@ -70,3 +70,3 @@ | ||
try { | ||
cb(null, fs.realpathSync(path)); | ||
cb(null, fs.realpathSync.native(path)); | ||
} | ||
@@ -92,3 +92,3 @@ catch (err) { | ||
if (body && typeof body === 'object' && body.realname) { | ||
filename = body.realname; // use fs.realpath name | ||
filename = body.realname; // use fs.realpath.native name | ||
body = fileCache[filename]; | ||
@@ -95,0 +95,0 @@ } |
@@ -17,37 +17,33 @@ 'use strict'; | ||
this.id = id; | ||
// TODO: If we require Node 12, use process.hrtime.bigint() | ||
// as process.hrtime() is deprecated. | ||
// eslint-disable-next-line no-multi-assign | ||
this.startTime = this.lapTime = process.hrtime(); | ||
this.startTime = process.hrtime.bigint(); | ||
this.lapTime = this.startTime; | ||
} | ||
/** | ||
* Start watch. | ||
*/ | ||
* Start watch. | ||
*/ | ||
start() { | ||
// eslint-disable-next-line no-multi-assign | ||
this.startTime = this.lapTime = process.hrtime(); | ||
this.startTime = process.hrtime.bigint(); | ||
this.lapTime = this.startTime; | ||
} | ||
/** | ||
* Stop and return delta T | ||
* Stop and return delta T in nanoseconds, | ||
* but do not set start time | ||
*/ | ||
stop() { | ||
return process.hrtime(this.startTime); | ||
const endTime = process.hrtime.bigint(); | ||
return endTime - this.startTime; | ||
} | ||
/** | ||
* return lap time | ||
*/ | ||
lap() { | ||
const dt = process.hrtime(this.lapTime); | ||
this.lapTime = process.hrtime(); | ||
const endTime = process.hrtime.bigint(); | ||
const dt = endTime - this.startTime; | ||
this.lapTime = process.hrtime.bigint(); | ||
return dt; | ||
} | ||
// stop as sec.ns float | ||
stopInFloatSecs() { | ||
const dt = this.stop(); | ||
return dt[0] + dt[1] / 1000000000; | ||
return dt / BigInt(1000000000); | ||
} | ||
@@ -58,3 +54,3 @@ | ||
const dt = this.lap(); | ||
return dt[0] + dt[1] / 1000000000; | ||
return dt / BigInt(1000000000); | ||
} | ||
@@ -68,3 +64,3 @@ } | ||
* | ||
* To enable time tracing, set CDSC_TIMETRACE to true in the environment | ||
* To enable time tracing, set CDSC_TIMETRACING to true in the environment | ||
* | ||
@@ -113,6 +109,9 @@ * @class TimeTracer | ||
const current = this.traceStack.pop(); | ||
const dT = current.stop(); | ||
const dt = current.stop(); | ||
const base = `${ ' '.repeat(this.traceStack.length * 2) }${ current.id } took:`; | ||
const sec = (dt / BigInt(1000000000)).toString(); | ||
// first, get remaining ns, then convert to ms. | ||
const msec = ((dt % BigInt(1000000000)) / BigInt(1000000)).toString(); | ||
// eslint-disable-next-line no-console | ||
console.error( `${ base }${ ' '.repeat(60 - base.length) } %ds %dms`, dT[0], dT[1] / 1000000); | ||
console.error( `${ base }${ ' '.repeat(60 - base.length) } %ds %dms`, sec, msec ); | ||
} | ||
@@ -119,0 +118,0 @@ catch (e) { |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "2.15.4", | ||
"version": "3.0.0", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -8,2 +8,3 @@ "homepage": "https://cap.cloud.sap/", | ||
"license": "SEE LICENSE IN LICENSE", | ||
"type": "commonjs", | ||
"bin": { | ||
@@ -17,3 +18,33 @@ "cdsc": "bin/cdsc.js", | ||
"scripts": { | ||
"postinstall": "node lib/fix_antlr4-8_warning.js" | ||
"download": "node scripts/downloadANTLR.js", | ||
"gen": "node ./scripts/build.js && node scripts/genGrammarChecksum.js", | ||
"xmakeBeforeInstall": "echo \"Due to binary mirror, use sqlite 5.0.8 explicitly\" && npm install --save --save-exact --no-package-lock sqlite3@5.0.8", | ||
"xmakeAfterInstall": "npm run gen", | ||
"xmakePrepareRelease": "echo \"$(node scripts/stripReadme.js README.md)\" > README.md && node scripts/assertSnapshotVersioning.js && node scripts/assertChangelog.js && node scripts/cleanup.js --remove-dev", | ||
"test": "node scripts/verifyGrammarChecksum.js && mocha --parallel --reporter min --reporter-option maxDiffSize=0 test/ test3/ && mocha scripts/testLazyLoading.js", | ||
"testverbose": "node scripts/verifyGrammarChecksum.js && mocha --parallel test/ test3/", | ||
"testdot": "node scripts/verifyGrammarChecksum.js && mocha --parallel --reporter dot --reporter-option maxDiffSize=0 --full-trace scripts/linter/lintMessages.js test/ test3/", | ||
"test3": "node scripts/verifyGrammarChecksum.js && node scripts/linter/lintTests.js test3/ && mocha --reporter-option maxDiffSize=0 scripts/linter/lintMessages.js test3/", | ||
"deployTest3SQL": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/testHANASQLDeployment.js", | ||
"deployTest3": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/testDeployment.js", | ||
"deployDiffs": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/deployDiffs.js", | ||
"gentest3": "cross-env MAKEREFS=${MAKEREFS:-'true'} mocha --reporter-option maxDiffSize=0 test3/testRefFiles.js", | ||
"testdb": "node scripts/verifyGrammarChecksum.js && cross-env TESTDB='TRUE' mocha", | ||
"testmigration": "npm install --no-save @sap/hana-client@2.3.123 @sap/hdi-deploy@3.10.0 && node scripts/verifyGrammarChecksum.js && cross-env TESTMIGRATION='TRUE' mocha", | ||
"testall": "npm install --no-save @sap/hana-client@2.3.123 @sap/hdi-deploy@3.10.0 && node scripts/verifyGrammarChecksum.js && cross-env TESTDB='TRUE' TESTMIGRATION='TRUE' mocha", | ||
"coverage": "cross-env nyc mocha --reporter-option maxDiffSize=0 test/ test3/ && nyc report --reporter=lcov", | ||
"lint": "eslint bin/ benchmark/ lib/ test/ test3/ scripts/ types/ && node scripts/linter/lintGrammar.js && node scripts/linter/lintTests.js test3/ && markdownlint README.md CHANGELOG.md doc/ internalDoc/ && cd share/messages && markdownlint .", | ||
"tslint": "tsc --pretty -p .", | ||
"updateVocs": "node scripts/odataAnnotations/generateDictMain.js && npm run generateAllRefs", | ||
"generateCompilerRefs": "cross-env MAKEREFS='true' mocha test/testCompiler.js", | ||
"generateEdmRefs": "cross-env MAKEREFS='true' mocha test/testEdmPositive.js", | ||
"generateForHanaRefs": "cross-env MAKEREFS='true' mocha test/testHanaTransformation.js", | ||
"generateOdataRefs": "cross-env MAKEREFS='true' mocha test/testODataTransformation.js", | ||
"generateOdataAnnoRefs": "cross-env MAKEREFS='true' mocha test/testODataAnnotations.js", | ||
"generateToSqlRefs": "cross-env MAKEREFS='true' mocha test/testToSql.js", | ||
"generateToRenameRefs": "cross-env MAKEREFS='true' mocha test/testToRename.js", | ||
"generateChecksRefs": "cross-env MAKEREFS='true' mocha test/testChecks.js", | ||
"generateScenarioRefs": "cross-env MAKEREFS='true' mocha test/testScenarios.js", | ||
"generateDraftRefs": "cross-env MAKEREFS='true' mocha test/testDraft.js", | ||
"generateAllRefs": "node scripts/verifyGrammarChecksum.js && cross-env MAKEREFS='true' mocha --reporter-option maxDiffSize=0 test/ test3/" | ||
}, | ||
@@ -24,3 +55,3 @@ "keywords": [ | ||
"dependencies": { | ||
"antlr4": "4.8.0" | ||
"antlr4": "4.9.3" | ||
}, | ||
@@ -38,4 +69,4 @@ "files": [ | ||
"engines": { | ||
"node": ">=12" | ||
"node": ">=14" | ||
} | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
4049160
80357
0
167
12
+ Addedantlr4@4.9.3(transitive)
- Removedantlr4@4.8.0(transitive)
Updatedantlr4@4.9.3