@sap/cds-compiler
Advanced tools
Comparing version 4.3.2 to 4.4.0
@@ -10,2 +10,31 @@ # ChangeLog for cds compiler and backends | ||
## Version 4.4.0 - 2023-11-09 | ||
### Added | ||
- compiler: International letters such as `ä` can now be used in CDS identifiers without quoting. | ||
Unicode letters similar to JavaScript are allowed. | ||
- to.edm(x): | ||
+ Allow to render all complex types within a requested service as `OpenType=true` with option `--odata-open-type`. | ||
Explicit `@open: false` annotations are not overwritten. | ||
+ Allow to annotate the generated draft artifacts but not generated foreign keys (as with any other managed association). | ||
- to.sql|hdi|hdbcds: Allow annotating the generated `.drafts` tables. | ||
### Changed | ||
- CDL parser: improve error recovery inside structured annotation values | ||
- Update OData vocabularies: 'Aggregation', 'Common', 'Core', 'Hierarchy', 'ODM', 'UI'. | ||
### Fixed | ||
- parser: | ||
+ `/**/` was incorrectly parsed as an unterminated doc-comment, leading to parse errors. | ||
+ Doc-comments consisting only of `*` were not correctly parsed. | ||
- compiler: do not propagate `default`s in a CSN of flavor `xtended`/`gensrc`. | ||
- to.hana: Fix various bugs in association to join translation. Support `$self` references | ||
in filter expressions. | ||
- to.edm(x): Omit `EntitySet` attribute on `Edm.FunctionImport` and `Edm.ActionImport` that return | ||
a singleton. | ||
- to.sql|hdi.migration: Improve handling of primary key changes - detect them and render corresponding drop-create. | ||
## Version 4.3.2 - 2023-10-25 | ||
@@ -12,0 +41,0 @@ |
@@ -40,2 +40,3 @@ /** @module API */ | ||
const { csn2edm, csn2edmAll } = require('../edm/csn2edm'); | ||
const { traceApi } = require('./trace'); | ||
@@ -150,3 +151,3 @@ const relevantGeneralOptions = [ /* for future generic options */ ]; | ||
function odata( csn, options = {} ) { | ||
traceApi("Options passed into 'for.odata'", options); | ||
traceApi('for.odata', options); | ||
const internalOptions = prepareOptions.for.odata(options); | ||
@@ -164,3 +165,3 @@ return odataInternal(csn, internalOptions); | ||
function cdl( csn, options = {} ) { | ||
traceApi("Options passed into 'to.cdl'", options); | ||
traceApi('to.cdl', options); | ||
const internalOptions = prepareOptions.to.cdl(options); | ||
@@ -257,3 +258,3 @@ return toCdl.csnToCdl(csn, internalOptions); | ||
function sql( csn, options = {} ) { | ||
traceApi("Options passed into 'to.sql'", options); | ||
traceApi('to.sql', options); | ||
const internalOptions = prepareOptions.to.sql(options); | ||
@@ -281,3 +282,3 @@ internalOptions.transformation = 'sql'; | ||
function hdi( csn, options = {} ) { | ||
traceApi("Options passed into 'to.hdi'", options); | ||
traceApi('to.hdi', options); | ||
const internalOptions = prepareOptions.to.hdi(options); | ||
@@ -385,3 +386,3 @@ | ||
function sqlMigration( csn, options, beforeImage ) { | ||
traceApi("Options passed into 'to.sql.migration'", options); | ||
traceApi('to.sql.migration', options); | ||
const internalOptions = prepareOptions.to.sql(options); | ||
@@ -407,2 +408,4 @@ const { | ||
Object.entries(diff.deletions).forEach(entry => diffFilterObj.deletion(entry, error)); | ||
diff.changedPrimaryKeys = diff.changedPrimaryKeys | ||
.filter(an => diffFilterObj.changedPrimaryKeys(an)); | ||
} | ||
@@ -540,3 +543,3 @@ | ||
function hdiMigration( csn, options, beforeImage ) { | ||
traceApi("Options passed into 'to.hdi.migration'", options); | ||
traceApi('to.hdi.migration', options); | ||
const internalOptions = prepareOptions.to.hdi(options); | ||
@@ -626,3 +629,3 @@ | ||
function hdbcds( csn, options = {} ) { | ||
traceApi("Options passed into 'to.hdbcds'", options); | ||
traceApi('to.hdbcds', options); | ||
const internalOptions = prepareOptions.to.hdbcds(options); | ||
@@ -644,3 +647,3 @@ internalOptions.transformation = 'hdbcds'; | ||
function edm( csn, options = {} ) { | ||
traceApi("Options passed into 'to.edm'", options); | ||
traceApi('to.edm', options); | ||
// If not provided at all, set service to undefined to trigger validation | ||
@@ -676,3 +679,3 @@ const internalOptions = prepareOptions.to.edm( | ||
function edmall( csn, options = {} ) { | ||
traceApi("Options passed into 'to.edm.all'", options); | ||
traceApi('to.edm.all', options); | ||
const internalOptions = prepareOptions.to.edm(options); | ||
@@ -708,3 +711,3 @@ const { error } = messages.makeMessageFunction(csn, internalOptions, 'for.odata'); | ||
function edmx( csn, options = {} ) { | ||
traceApi("Options passed into 'to.edmx'", options); | ||
traceApi('to.edmx', options); | ||
// If not provided at all, set service to undefined to trigger validation | ||
@@ -741,3 +744,3 @@ const internalOptions = prepareOptions.to.edmx( | ||
function edmxall( csn, options = {} ) { | ||
traceApi("Options passed into 'to.edmx.all'", options); | ||
traceApi('to.edmx.all', options); | ||
const internalOptions = prepareOptions.to.edmx(options); | ||
@@ -859,15 +862,2 @@ | ||
/** | ||
* Print args to stderr if CDSC_TRACE_API is set | ||
* | ||
* @param {...any} args to be logged to stderr | ||
*/ | ||
function traceApi( ...args ) { | ||
if (process?.env?.CDSC_TRACE_API !== undefined) { | ||
for (const arg of args) { | ||
// eslint-disable-next-line no-console | ||
console.error( `${ typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg }`); | ||
} | ||
} | ||
} | ||
@@ -874,0 +864,0 @@ module.exports = { |
@@ -45,2 +45,3 @@ 'use strict'; | ||
'odataVocabularies', | ||
'odataOpenType', | ||
'service', | ||
@@ -47,0 +48,0 @@ 'serviceNames', |
@@ -79,3 +79,3 @@ 'use strict'; | ||
function weakLocation( loc ) { | ||
return { | ||
return (!loc?.endLine) ? loc : { | ||
__proto__: CsnLocation.prototype, | ||
@@ -91,2 +91,45 @@ file: loc.file, | ||
/** | ||
* Return a location to be used for compiler-generated artifacts whose location is | ||
* best derived from a reference (`type`, `includes`, `target`, `value`) or a name. | ||
* Omit the end position to indicate that this is just an approximate location. | ||
* | ||
* If represented by a `path` (not always the case for a `name`), use the location | ||
* of its last item. Reason: think of an IDE functionality “Go to Definition” – only | ||
* a double-click on the _last_ identifier token of the reference jumps to the artifact | ||
* represented by the complete reference. | ||
* | ||
* @param {CsnLocation} loc | ||
* @returns {CsnLocation} | ||
*/ | ||
function weakRefLocation( ref ) { | ||
if (!ref) | ||
return ref; | ||
const { path } = ref; | ||
const loc = path?.length ? path[path.length - 1].location : ref.location; | ||
return (!loc?.endLine) ? loc : { | ||
__proto__: CsnLocation.prototype, | ||
file: loc.file, | ||
line: loc.line, | ||
col: loc.col, | ||
endLine: undefined, | ||
endCol: undefined, | ||
}; | ||
} | ||
/** | ||
* @param {CsnLocation} loc | ||
* @returns {CsnLocation} | ||
*/ | ||
function weakEndLocation( loc ) { | ||
return loc && { | ||
__proto__: CsnLocation.prototype, | ||
file: loc.file, | ||
line: loc.endLine, | ||
col: loc.endCol && loc.endCol - 1, | ||
endline: undefined, | ||
endCol: undefined, | ||
}; | ||
} | ||
/** | ||
* Returns a dummy location for built-in definitions. | ||
@@ -177,2 +220,4 @@ * | ||
weakLocation, | ||
weakRefLocation, | ||
weakEndLocation, | ||
builtinLocation, | ||
@@ -179,0 +224,0 @@ dictLocation, |
@@ -431,4 +431,5 @@ // Functions and classes for syntax messages | ||
let semanticLocation = location[1] ? homeName( location[1], false ) : null; | ||
if (location[2]) // optional suffix | ||
semanticLocation += `/${ location[2] }`; | ||
if (location[2]) { // optional suffix, e.g. annotation | ||
semanticLocation += `/${ (typeof location[2] === 'string') ? location[2]: homeName(location[2]) }`; | ||
} | ||
@@ -680,2 +681,3 @@ const definition = location[1] ? homeName( location[1], true ) : null; | ||
meta: quote.angle, | ||
othermeta: quote.angle, | ||
keyword, | ||
@@ -1578,2 +1580,5 @@ // more complex convenience: | ||
} | ||
else if (step === 'targetAspect') { | ||
// skip | ||
} | ||
else if (step === 'xpr' || step === 'ref' || step === 'as' || step === 'value') { | ||
@@ -1614,3 +1619,3 @@ break; // don't go into xprs, refs, aliases, values, etc. | ||
if (options.testMode) | ||
throw new CompilerAssertion(`semantic location: Missing segment: ${ csnPath[index] } for path ${ JSON.stringify( csnPath) }`); | ||
throw new CompilerAssertion(`semantic location: Unhandled segment: ${ csnPath[index] } for path ${ JSON.stringify( csnPath) }`); | ||
break; | ||
@@ -1617,0 +1622,0 @@ } |
@@ -8,2 +8,3 @@ { | ||
"rules": { | ||
"cds-compiler/message-no-quotes": "off", | ||
"quotes": ["error", "single", { | ||
@@ -10,0 +11,0 @@ "avoidEscape": true, |
@@ -140,3 +140,3 @@ 'use strict'; | ||
{ type: typeName, kind: type.kind, service: serviceName }, | ||
'$(TYPE) of kind $(KIND) is defined outside a service and can\'t be used in $(SERVICE)'); | ||
'Referenced $(KIND) $(TYPE) can\'t be used in service $(SERVICE) because it is not defined in $(SERVICE)'); | ||
// } | ||
@@ -143,0 +143,0 @@ } |
@@ -27,4 +27,4 @@ 'use strict'; | ||
if (member['@Core.MediaType'] && member.type && !(this.csnUtils.getFinalTypeInfo(member.type)?.type in allowedCoreMediaTypes)) { | ||
this.warning(null, member.$path, { names: [ 'Edm.String', 'Edm.Binary' ] }, | ||
'Element annotated with “@Core.MediaType” should be of a type mapped to $(NAMES)'); | ||
this.warning(null, member.$path, { anno: '@Core.MediaType', names: [ 'Edm.String', 'Edm.Binary' ] }, | ||
'Element annotated with $(ANNO) should be of a type mapped to $(NAMES)'); | ||
} | ||
@@ -31,0 +31,0 @@ } |
@@ -48,7 +48,10 @@ 'use strict'; | ||
const transformers = { | ||
/* | ||
columns: aTCB, | ||
groupBy: aTCB, | ||
orderBy: aTCB, | ||
having: aTCB, | ||
where: aTCB, | ||
*/ | ||
orderBy: aTCB, // filters in order by imply a join, not allowed | ||
from: aTCB, // $self refs in from clause filters are not allowed | ||
}; | ||
@@ -55,0 +58,0 @@ |
@@ -476,3 +476,3 @@ // Consistency checker on model (XSN = augmented CSN) | ||
// expressions as annotation values | ||
'$tokenTexts', 'op', 'args', 'func', | ||
'$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', | ||
], | ||
@@ -683,2 +683,3 @@ // TODO: restrict path to #simplePath | ||
'localized-entity', // `.texts` entity | ||
'localized-origin', // `.texts` entity | ||
'nav', // only used for MASKED, TODO(v5): Remove | ||
@@ -971,3 +972,3 @@ 'none', // only used in ensureColumnName(): Used in object representing empty alias | ||
return function isOneOfInner( node, parent, prop ) { | ||
if (!values.includes( node )) | ||
if (!values.includes( node ) && node !== undefined) | ||
throw new InternalConsistencyError( `Unexpected value '${ node }', expected ${ JSON.stringify( values ) }${ at( [ node, parent ], prop ) }` ); | ||
@@ -974,0 +975,0 @@ }; |
@@ -85,3 +85,3 @@ // Base Definitions for the Core Compiler | ||
return name; | ||
if (!art.kind) // annotation assignments | ||
if (!art.kind) // annotation assignments, $self param type | ||
return { ...art.name, absolute: art.name.id }; | ||
@@ -88,0 +88,0 @@ if (art.kind === 'using') |
@@ -65,2 +65,15 @@ // The builtin artifacts of CDS | ||
/** | ||
* Properties that are required next to `=` to make an annotation value an actual expression | ||
* and not some foreign structure. | ||
* | ||
* @type {string[]} | ||
*/ | ||
const xprInAnnoProperties = [ | ||
'ref', 'xpr', 'list', 'literal', 'val', | ||
'#', 'func', 'args', 'SELECT', 'SET', | ||
'cast', | ||
]; | ||
// const hana = { | ||
@@ -182,3 +195,3 @@ // BinaryFloat: {}, | ||
// id and locale are always available | ||
elements: { id: {}, locale: {} }, | ||
elements: { id: {}, locale: {}, tenant: {} }, | ||
// Allow $user.<any> | ||
@@ -307,2 +320,11 @@ $uncheckedElements: true, | ||
/** | ||
* Return whether JSON object `val` is a representation for an annotation expression | ||
*/ | ||
function isAnnotationExpression( val ) { | ||
// TODO: we might allow `'=': true`, not just a string, for expressions, to be | ||
// decided → just check truthy at the moment | ||
return val['='] && xprInAnnoProperties.some( prop => val[prop] !== undefined ); | ||
} | ||
/** | ||
* Check that the given time is within boundaries. | ||
@@ -541,2 +563,3 @@ * Checks according to ISO 8601. | ||
typeParameters, | ||
xprInAnnoProperties, | ||
functionsWithoutParens, | ||
@@ -546,2 +569,3 @@ specialFunctions, | ||
initBuiltins, | ||
isAnnotationExpression, | ||
isInReservedNamespace, | ||
@@ -548,0 +572,0 @@ isBuiltinType, |
@@ -217,2 +217,3 @@ // Checks on XSN performed during compile() that are useful for the user | ||
// "key" keyword at localized element in SELECT list. | ||
// TODO: not in inferred elements, but also inside aspects | ||
if (elem.key?.val && elem._main?.query) { | ||
@@ -463,3 +464,3 @@ // original element is localized but not key, as that would have | ||
} | ||
if (elem.default) { | ||
if (elem.default?.val !== undefined) { | ||
if (elem.targetAspect || elem.on || fkCount !== 1) { | ||
@@ -857,3 +858,3 @@ const variant = (elem.targetAspect && 'targetAspect') || (elem.on && 'onCond') || 'multi'; | ||
if (!elementDecl) { | ||
warning( null, [ anno.location || anno.name.location, art ], | ||
warning( null, [ anno.location || anno.name.location, art, anno ], | ||
{ name: anno.name.id, anno: annoDecl.name.id }, | ||
@@ -872,7 +873,7 @@ 'Element $(NAME) not found for annotation $(ANNO)' ); | ||
if (elementDecl.type?._artifact) { | ||
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ], | ||
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ], | ||
{ '#': 'type', type: elementDecl.type._artifact } ); | ||
} | ||
else { | ||
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ], | ||
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art, anno ], | ||
{ '#': 'std', anno: anno.name.id } ); | ||
@@ -898,3 +899,3 @@ } | ||
const anno = annoDef.name.id; | ||
const loc = [ value.location || value.name.location, art ]; | ||
const loc = [ value.location || value.name.location, art, annoDef ]; | ||
@@ -901,0 +902,0 @@ // Array expected? |
@@ -139,3 +139,3 @@ // Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN | ||
pathName, | ||
isDirectComposition, | ||
targetCantBeAspect, | ||
} = require('./utils'); | ||
@@ -317,3 +317,2 @@ const { compareLayer } = require('./moduleLayers'); | ||
name: { id: using, location, $inferred: 'as' }, | ||
// TODO: use global ref (in general - all uses of splitIntoPath) | ||
extern: { location, id: absolute }, | ||
@@ -569,3 +568,3 @@ location, | ||
if (art.params) | ||
initDollarParameters( art ); | ||
initDollarParameters( art ); // $parameters | ||
@@ -1205,3 +1204,3 @@ if (!art.query) | ||
// TODO: make special for extend/annotate | ||
// TODO: is only necessary for extensions - make special for extend/annotate | ||
function checkDefinitions( construct, parent, prop, dict = construct[prop] ) { | ||
@@ -1234,3 +1233,3 @@ // TODO: do differently, see also annotateMembers() in resolver | ||
error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, { | ||
std: 'Actions and functions can\'t be extended, only annotated', | ||
std: 'Actions and functions can\'t be extended, only annotated', // TODO: → ext-unsupported | ||
action: 'Actions can\'t be extended, only annotated', | ||
@@ -1285,13 +1284,16 @@ function: 'Functions can\'t be extended, only annotated', | ||
* Return whether the `target` is actually a `targetAspect` | ||
* TODO: really do that here and not in kick-start.js? | ||
*/ | ||
function targetIsTargetAspect( elem ) { | ||
const { target } = elem; | ||
if (target.elements) { | ||
// TODO: error if CSN has both target.elements and targetAspect.elements | ||
// -> delete target | ||
if (target.elements) // CSN parser ensures: has no targetAspect then | ||
return true; | ||
if (elem.targetAspect) { | ||
// Ensure that a compiled CSN is parseable - not inside query, only on element | ||
return false; | ||
} | ||
if (elem.targetAspect || options.parseCdl || !isDirectComposition( elem )) | ||
if (targetCantBeAspect( elem ) || options.parseCdl) | ||
return false; | ||
// Compare this check with check in acceptEntity() called by resolvePath() | ||
// Remark: do not check `on` and `foreignKeys` here, we want error for those, not the aspect | ||
const name = resolveUncheckedPath( target, 'target', elem ); | ||
@@ -1298,0 +1300,0 @@ const aspect = name && model.definitions[name]; |
@@ -5,3 +5,3 @@ // Extend | ||
const { weakLocation } = require('../base/location'); | ||
const { weakRefLocation } = require('../base/location'); | ||
const { searchName } = require('../base/messages'); | ||
@@ -690,17 +690,11 @@ const { | ||
// const unexpected_props = { | ||
// elements: 'anno-unexpected-elements', | ||
// enum: 'anno-unexpected-elements', // TODO | ||
// params: 'anno-unexpected-params', | ||
// actions: 'anno-unexpected-actions', | ||
// }; | ||
// const undefined_props = { | ||
// elements: 'anno-undefined-element', | ||
// enum: 'anno-undefined-element', // TODO | ||
// params: 'anno-undefined-param', | ||
// actions: 'anno-undefined-action', | ||
// }; | ||
function checkRemainingMemberExtensions( parent, ext, prop, name ) { | ||
// console.log('CRME:',prop,name,parent,ext) | ||
// TODO: just use `ext-undefined-element` etc also when no elements are there | ||
// at all (but use an extra text variant and the `{…}` location). Reason: we | ||
// might allow to add new actions, and an `annotate` on an undefined action | ||
// should not lead to another message id. We would use and extra message id | ||
// if we consider this an error or such sub annotates are then ignored | ||
// (i.e. not put into the "super annotate"). | ||
const dict = parent[prop]; | ||
@@ -719,2 +713,5 @@ if (!dict) { | ||
entity: 'Elements of entity types can\'t be annotated', | ||
// TODO: extra msg for 'entity'? → this is some other | ||
// situation, somehow similar when trying to annotate elements | ||
// of target entity | ||
} ); | ||
@@ -727,4 +724,4 @@ break; | ||
case 'actions': | ||
// TODO: check if artifact can have actions, similar to `anno-unexpected-actions` | ||
notFound( 'anno-undefined-action', ext.name.location, ext, | ||
// TODO: use extra text variant and location of dictionary | ||
notFound( 'ext-undefined-action', ext.name.location, ext, | ||
{ '#': 'action', art: parent, name } ); | ||
@@ -744,3 +741,3 @@ break; | ||
case 'elements': | ||
notFound( 'anno-undefined-element', ext.name.location, ext, | ||
notFound( 'ext-undefined-element', ext.name.location, ext, | ||
{ '#': (inReturns ? 'returns' : 'element'), art, name }, | ||
@@ -750,3 +747,3 @@ parent.elements ); | ||
case 'enum': // TODO: extra msg id? | ||
notFound( 'anno-undefined-element', ext.name.location, ext, | ||
notFound( 'ext-undefined-element', ext.name.location, ext, | ||
{ '#': (inReturns ? 'enum-returns' : 'enum'), art, name }, | ||
@@ -756,3 +753,3 @@ parent.enum ); | ||
case 'params': | ||
notFound( 'anno-undefined-param', ext.name.location, ext, | ||
notFound( 'ext-undefined-param', ext.name.location, ext, | ||
{ '#': 'param', art: parent, name }, | ||
@@ -762,3 +759,3 @@ parent.params ); | ||
case 'actions': | ||
notFound( 'anno-undefined-action', ext.name.location, ext, | ||
notFound( 'ext-undefined-action', ext.name.location, ext, | ||
{ '#': 'action', art: parent, name }, | ||
@@ -1164,4 +1161,6 @@ parent.actions ); | ||
const members = ext[prop]; | ||
if (members) | ||
if (members) { | ||
ext[prop] = Object.create( null ); | ||
ext[prop][$location] = members[$location]; | ||
} | ||
let hasNewElement = false; | ||
@@ -1174,2 +1173,3 @@ | ||
ext[prop] = Object.create( null ); | ||
const location = weakRefLocation( ref ); | ||
// eslint-disable-next-line no-loop-func | ||
@@ -1183,3 +1183,3 @@ forEachInOrder( template, prop, ( origin, name ) => { | ||
hasNewElement = true; | ||
const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) ); | ||
const elem = linkToOrigin( origin, name, parent, prop, location ); | ||
setLink( elem, '_block', origin._block ); | ||
@@ -1186,0 +1186,0 @@ if (!parent) // not yet set for EXTEND foo WITH bar => linkToOrigin() did not add it |
@@ -89,3 +89,3 @@ // Things which needs to done for parse.cdl after define() | ||
// define.js takes care that `target` is a ref and | ||
// `targetAspect` is a structure. | ||
// `targetAspect` is a structure (anonymous aspect) or ref to aspect. | ||
if (artifact.target) | ||
@@ -92,0 +92,0 @@ resolveUncheckedPath( artifact.target, 'target', main ); |
@@ -20,4 +20,6 @@ // Generate: localized data and managed compositions | ||
} = require('./utils'); | ||
const { weakLocation } = require('../base/location'); | ||
const { weakLocation, weakRefLocation, weakEndLocation } = require('../base/location'); | ||
const $location = Symbol.for( 'cds.$location' ); | ||
function generate( model ) { | ||
@@ -224,2 +226,3 @@ const { options } = model; | ||
'Keyword $(KEYWORD) is ignored for primary keys' ); | ||
// continuation semantics as stated: counts as key field in texts entity | ||
} | ||
@@ -282,7 +285,7 @@ } | ||
function createTextsEntity( base, absolute, textElems, fioriEnabled ) { | ||
const { location } = base.name; | ||
const location = weakLocation( base.elements[$location] || base.location ); | ||
const art = { | ||
kind: 'entity', | ||
name: { id: absolute, location }, | ||
location: base.location, | ||
location, | ||
elements: Object.create( null ), | ||
@@ -338,26 +341,4 @@ $inferred: 'localized-entity', | ||
for (const orig of textElems) { | ||
const elem = linkToOrigin( orig, orig.name.id, art, 'elements' ); | ||
if (orig.key && orig.key.val) { | ||
// elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location }; | ||
// TODO: the previous would be better, but currently not supported in toCDL | ||
if (!fioriEnabled) { | ||
elem.key = { val: true, $inferred: 'localized', location }; | ||
// If the propagated elements remain key (that is not fiori.draft.enabled) | ||
// they should be omitted from OData containment EDM | ||
setAnnotation( elem, '@odata.containment.ignore', location ); | ||
} | ||
else { | ||
// add the former key paths to the unique constraint | ||
assertUniqueValue.push({ | ||
path: [ { id: orig.name.id, location: orig.location } ], | ||
location: orig.location, | ||
}); | ||
} | ||
} | ||
if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword | ||
const localized = orig.localized || orig.type || orig.name; | ||
elem.localized = { val: null, $inferred: 'localized', location: localized.location }; | ||
} | ||
} | ||
for (const orig of textElems) | ||
addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ); | ||
@@ -373,3 +354,3 @@ initArtifact( art ); | ||
// Because ID_texts is not copied from TextsAspect, the order is messed | ||
// up. Fix it. | ||
// up. Fix it. TODO: introduce $includeAfter from Extensions.md | ||
const { elements } = art; | ||
@@ -393,2 +374,27 @@ art.elements = Object.create( null ); | ||
function addElementToTextsEntity( orig, art, fioriEnabled, assertUniqueValue ) { | ||
const elem = linkToOrigin( orig, orig.name.id, art, 'elements' ); | ||
// To keep the locations of non-inferred original elements, do not set $inferred: | ||
if (orig.$inferred) | ||
elem.$inferred = 'localized-origin'; | ||
const { location } = elem; | ||
if (orig.key && orig.key.val) { | ||
// elem.key = { val: fioriEnabled ? null : true, $inferred: 'localized', location }; | ||
// TODO: the previous would be better, but currently not supported in toCDL | ||
if (!fioriEnabled) { | ||
elem.key = { val: true, $inferred: 'localized', location }; | ||
// If the propagated elements remain key (that is not fiori.draft.enabled) | ||
// they should be omitted from OData containment EDM | ||
setAnnotation( elem, '@odata.containment.ignore', location ); | ||
} | ||
else { | ||
// add the former key paths to the unique constraint | ||
assertUniqueValue.push( { path: [ { id: orig.name.id, location } ], location } ); | ||
} | ||
} | ||
if (hasTruthyProp( orig, 'localized' )) { // use location of LOCALIZED keyword | ||
elem.localized = { val: null, $inferred: 'localized', location }; | ||
} | ||
} | ||
/** | ||
@@ -409,5 +415,5 @@ * Enrich the `.texts` entity for the given base artifact. | ||
const textsAspect = model.definitions['sap.common.TextsAspect']; | ||
const { location } = art.name; | ||
const { location } = art; | ||
art.includes = [ createInclude( textsAspectName, base.location ) ]; | ||
art.includes = [ createInclude( textsAspectName, location ) ]; | ||
@@ -418,2 +424,3 @@ if (fioriEnabled) { | ||
linkToOrigin( textsAspect.elements.locale, 'locale', art, 'elements', location ); | ||
art.elements.locale.$inferred = 'localized'; | ||
} | ||
@@ -436,3 +443,3 @@ | ||
const hasLocaleType = model.definitions['sap.common.Locale']?.kind === 'type'; | ||
const { location } = art.name; | ||
const { location } = art; // is already a weak location | ||
const locale = { | ||
@@ -443,2 +450,3 @@ name: { location, id: 'locale' }, | ||
location, | ||
$inferred: 'localized', // $generated in Universal CSN, no $location | ||
}; | ||
@@ -462,3 +470,3 @@ if (!hasLocaleType) | ||
const keys = textElems.filter( e => e.key && e.key.val ); | ||
const { location } = art.name; | ||
const location = weakEndLocation( art.elements[$location] ) || weakLocation( art.location ); | ||
const texts = { | ||
@@ -582,3 +590,3 @@ name: { location, id: 'texts' }, | ||
let target = origin.targetAspect; | ||
if (target && target.path) | ||
if (target?.path) | ||
target = resolvePath( origin.targetAspect, 'targetAspect', origin ); | ||
@@ -616,3 +624,3 @@ if (!target || !target.elements) | ||
return false; // no elements or with redefinitions | ||
const location = elem.target && elem.target.location || elem.location; | ||
const location = elem.targetAspect?.location || elem.location; | ||
if ((elem._main._upperAspects || []).includes( target )) | ||
@@ -664,5 +672,11 @@ return 0; // circular containment of the same aspect | ||
// TODO: Make it configurable error; v5: error | ||
warning( 'def-expected-comp-aspect', [ elem.type.location, elem ], | ||
{ prop: 'Composition of', otherprop: 'Association to' }, | ||
'Expected $(PROP), but found $(OTHERPROP) for composition of aspect' ); | ||
// TODO: move to resolve.js where we test the targetAspect, | ||
warning( 'type-expecting-composition', [ elem.type.location, elem ], | ||
{ newcode: 'Composition of', code: 'Association to' }, | ||
'Expecting $(NEWCODE), not $(CODE) for the anonymous target aspect' ); | ||
// auto-correct to avoid additional error 'type-unexpected-target-aspect' if | ||
// cds.Association: | ||
const { path, $inferred } = elem.type; | ||
if (!$inferred && path?.length === 1 && path[0].id === 'cds.Association') | ||
path[0].id = 'cds.Composition'; | ||
} | ||
@@ -674,3 +688,3 @@ | ||
function createTargetEntity( target, elem, keys, entityName, base ) { | ||
const { location } = elem.targetAspect || elem.target || elem; | ||
const location = weakRefLocation( elem.targetAspect || elem.target || elem ); | ||
elem.on = { | ||
@@ -691,3 +705,3 @@ location, | ||
// for code navigation (e.g. via `extend`s): point to the element's name | ||
location: elem.name.location, | ||
location: weakLocation( elem.name.location ), | ||
}, | ||
@@ -709,14 +723,13 @@ location, | ||
// Since there is no user-written up_ element, use a weak location to the beginning of {…}. | ||
const upLocation = weakLocation( location ); | ||
const up = { // elements.up_ = ... | ||
name: { location: upLocation, id: 'up_' }, | ||
name: { location, id: 'up_' }, | ||
kind: 'element', | ||
location: upLocation, | ||
location, | ||
$inferred: 'aspect-composition', | ||
type: linkMainArtifact( upLocation, 'cds.Association' ), | ||
target: linkMainArtifact( upLocation, base.name.id ), | ||
type: linkMainArtifact( location, 'cds.Association' ), | ||
target: linkMainArtifact( location, base.name.id ), | ||
cardinality: { | ||
targetMin: { val: 1, literal: 'number', location: upLocation }, | ||
targetMax: { val: 1, literal: 'number', location: upLocation }, | ||
location: upLocation, | ||
targetMin: { val: 1, literal: 'number', location }, | ||
targetMax: { val: 1, literal: 'number', location }, | ||
location, | ||
}, | ||
@@ -730,9 +743,9 @@ }; | ||
'up__', '@odata.containment.ignore' ); | ||
up.on = augmentEqual( upLocation, 'up_', Object.values( keys ), 'up__' ); | ||
up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' ); | ||
} | ||
else { | ||
up.key = { location: upLocation, val: true }; | ||
up.key = { location, val: true }; | ||
// managed associations must be explicitly set to not null | ||
// even if target cardinality is 1..1 | ||
up.notNull = { location: upLocation, val: true }; | ||
up.notNull = { location, val: true }; | ||
} | ||
@@ -742,4 +755,7 @@ | ||
// Only for named aspects, use a new location; otherwise use the origin's one. | ||
addProxyElements( art, target.elements, 'aspect-composition', target.name && location ); | ||
// To keep the locations of non-inferred original elements, do not set $inferred: | ||
const enforceLocation = target.name || elem.$inferred; | ||
addProxyElements( art, target.elements, 'aspect-composition', enforceLocation && location ); | ||
setLink( art, '_block', model.$internal ); | ||
@@ -764,3 +780,4 @@ model.definitions[entityName] = art; | ||
setLink( proxy, '_block', origin._block ); | ||
proxy.$inferred = inferred; | ||
if (location) | ||
proxy.$inferred = inferred; | ||
if (origin.masked) | ||
@@ -807,3 +824,3 @@ proxy.masked = Object.assign( { $inferred: 'include' }, origin.masked ); | ||
function linkMainArtifact( location, absolute ) { | ||
const r = { location }; | ||
const r = { location, path: [ { id: absolute, location } ] }; | ||
setArtifactLink( r, model.definitions[absolute] ); | ||
@@ -810,0 +827,0 @@ return r; |
@@ -6,3 +6,8 @@ // Kick-start: prepare to resolve all references | ||
const { isBetaEnabled, forEachGeneric } = require('../base/model'); | ||
const { setLink, annotationVal, annotationIsFalse } = require('./utils'); | ||
const { | ||
setLink, | ||
annotationVal, | ||
annotationIsFalse, | ||
isDirectComposition, | ||
} = require('./utils'); | ||
@@ -126,6 +131,3 @@ function kickStart( model ) { | ||
function tagCompositionTargets( elem ) { | ||
const { type } = elem; | ||
if (elem.target && type && | ||
(type._artifact === model.definitions['cds.Composition'] || | ||
type.path?.[0].id === 'cds.Composition')) { | ||
if (elem.target && isDirectComposition( elem )) { | ||
// A target aspect would have already moved to property `targetAspect` in | ||
@@ -132,0 +134,0 @@ // define.js (hm... more something for kick-start.js...) |
@@ -29,3 +29,3 @@ // Populate views with elements, elements with association targets, ... | ||
} = require('../base/dictionaries'); | ||
const { weakLocation } = require('../base/location'); | ||
const { weakLocation, weakRefLocation } = require('../base/location'); | ||
const { CompilerAssertion } = require('../base/error'); | ||
@@ -40,3 +40,2 @@ | ||
annotationLocation, | ||
augmentPath, | ||
linkToOrigin, | ||
@@ -118,2 +117,3 @@ setMemberParent, | ||
function traverseElementEnvironments( art ) { | ||
// TODO: we could leave out foreign keys (but they are traversed via forEachMember) | ||
let type = effectiveType( art ); | ||
@@ -382,4 +382,3 @@ while (type?.items) | ||
} | ||
const ref = art.type || art.value || art.name; | ||
const location = ref && ref.location || art.location; | ||
const location = weakRefLocation( art.type || art.value ) || weakLocation( art.location ); | ||
art.items = { $inferred: 'expanded', location }; | ||
@@ -410,6 +409,6 @@ setLink( art.items, '_outer', art ); | ||
const ref = art.type || art.value || art.name; | ||
const location = ref && ref.location || art.location; | ||
const location = weakRefLocation( ref ) || weakLocation( art.location ); | ||
// console.log( message( null, location, art, {target:struct,art}, 'Info','EXPAND-ELEM') | ||
// .toString(), Object.keys(struct.elements)) | ||
proxyCopyMembers( art, 'elements', struct.elements, weakLocation( location ), | ||
proxyCopyMembers( art, 'elements', struct.elements, location, | ||
null, isDeprecatedEnabled( options, 'noKeyPropagationWithExpansions' ) ); | ||
@@ -429,4 +428,4 @@ // Set elements expansion status (the if condition is always true, as no | ||
const ref = art.type || art.value || art.name; | ||
const location = weakLocation( ref && ref.location || art.location ); | ||
proxyCopyMembers( art, 'enum', origin.enum, weakLocation( location ) ); | ||
const location = weakRefLocation( ref ) || weakLocation( art.location ); | ||
proxyCopyMembers( art, 'enum', origin.enum, location ); | ||
// Set elements expansion status (the if condition is always true, as no | ||
@@ -530,2 +529,5 @@ // elements expansion will take place on artifact with existing other | ||
const selem = art.elements$ ? art.elements$[id] : art.enum$[id]; // specified element | ||
// TODO: the positions are very strange, at least for enums | ||
// see e.g. for test3/Queries/SpecifiedElements/SpecifiedElements.err.csn | ||
// better to complain at the end position of the enum dict | ||
if (!selem) { | ||
@@ -809,3 +811,5 @@ info( 'query-missing-element', [ ielem.name.location, art ], { | ||
const { elements } = query.items || query; | ||
let location = wildcard.location || query.from && query.from.location || query.location; | ||
let location = wildcard.location || | ||
weakRefLocation( query.from ) || | ||
weakLocation( query.location ); | ||
const inferred = query._main.$inferred; | ||
@@ -856,4 +860,8 @@ const excludingDict = (colParent || query).excludingDict || Object.create( null ); | ||
location = weakLocation( location ); | ||
// Usually, the location of a `*`-inferred element is the location of the `*`. | ||
// For inferred entities, it is the location of the corresponding source elem | ||
// (from all generated entities, only auto-exposed are “wildcard projections”): | ||
const elemLocation = !query._main.$inferred && location; | ||
const origin = envParent ? navElem : navElem._origin; | ||
const elem = linkToOrigin( origin, name, query, null, location ); | ||
const elem = linkToOrigin( origin, name, query, null, elemLocation ); | ||
// TODO: check assocToMany { * } | ||
@@ -864,8 +872,9 @@ dictAdd( elements, name, elem, ( _name, loc ) => { | ||
} ); | ||
elem.$inferred = '*'; | ||
elem.name.$inferred = '*'; | ||
if (!query._main.$inferred || origin.$inferred) | ||
elem.$inferred = '*'; | ||
elem.name.$inferred = '*'; // matters for A2J | ||
if (envParent) | ||
setWildcardExpandInline( elem, envParent, origin, name, location ); | ||
else | ||
setElementOrigin( elem, navElem, name, location ); | ||
setElementOrigin( elem, navElem, name, elem.location ); | ||
} | ||
@@ -1306,8 +1315,8 @@ } | ||
// console.log(absolute) | ||
const { location } = target.name; | ||
const from = augmentPath( location, target.name.id ); | ||
const location = weakRefLocation( target.name ); | ||
const from = { path: [ { id: target.name.id, location } ], location }; | ||
let art = { | ||
kind: 'entity', | ||
name: { location, id: absolute }, | ||
location: target.location, | ||
location, | ||
query: { location, op: { val: 'SELECT', location }, from }, | ||
@@ -1325,10 +1334,10 @@ $syntax: 'projection', | ||
// is art.query.from.path[0].$syntax: ':' required? | ||
art.query.from.path[0].args = Object.create( null ); | ||
from.path[0].args = Object.create( null ); | ||
forEachGeneric( target, 'params', (p, pn) => { | ||
art.params[pn] = linkToOrigin( p, pn, art, 'params', p.location ); | ||
art.query.from.path[0].args[pn] = { | ||
name: { id: p.name.id, location: p.location }, | ||
location: p.location, | ||
art.params[pn] = linkToOrigin( p, pn, art, 'params' ); | ||
from.path[0].args[pn] = { | ||
name: { id: p.name.id, location }, | ||
location, | ||
scope: 'param', | ||
path: [ { id: pn, location: p.location } ], | ||
path: [ { id: pn, location } ], | ||
}; | ||
@@ -1335,0 +1344,0 @@ } ); |
@@ -51,3 +51,3 @@ // Propagate properties in XSN | ||
targetElement: onlyViaParent, // in foreign keys | ||
value: onlyViaParent, // enum symbol value | ||
value: onlyViaParent, // enum symbol value, calculated element | ||
// masked: special = done in definer | ||
@@ -252,3 +252,6 @@ // key: special = done in resolver | ||
member.$inferred = 'proxy'; | ||
setEffectiveType( member, dict[name] ); | ||
if (prop === 'foreignKeys') | ||
setLink( member, '_effectiveType', member ); | ||
else | ||
setEffectiveType( member, dict[name] ); | ||
} | ||
@@ -385,2 +388,3 @@ target[prop][$inferred] = 'prop'; | ||
function setEffectiveType( target, source ) { | ||
// TODO: when is this already set? | ||
if (source._effectiveType !== undefined) | ||
@@ -387,0 +391,0 @@ setLink( target, '_effectiveType', source._effectiveType ); |
@@ -9,3 +9,3 @@ // Tweak associations: rewrite keys and on conditions | ||
} = require('../base/model'); | ||
const { dictLocation, weakLocation } = require('../base/location'); | ||
const { dictLocation, weakLocation, weakRefLocation } = require('../base/location'); | ||
@@ -46,3 +46,3 @@ const { | ||
// Think hard whether an on condition rewrite can lead to a new cyclic | ||
// dependency. If so, we need other messages anyway. TODO: probably do | ||
// dependency. If so, we need other messages anyway. TODO: probably dox | ||
// another cyclic check with testMode.js | ||
@@ -78,3 +78,3 @@ forEachUserArtifact( model, 'definitions', function check( art ) { | ||
if (art._service) | ||
forEachGeneric( art, 'elements', excludeAssociation ); | ||
forEachGeneric( art, 'elements', complainAboutTargetOutsideService ); | ||
@@ -101,8 +101,10 @@ traverseQueryPost( art.query, false, ( query ) => { | ||
function excludeAssociation( elem ) { | ||
function complainAboutTargetOutsideService( elem ) { | ||
const target = elem.target && elem.target._artifact; | ||
if (!target || target._service) // assoc to other service is OK | ||
return; | ||
if (!elem.$inferred) { // && !elem.target.$inferred | ||
info( 'assoc-target-not-in-service', [ elem.target.location, elem ], | ||
const loc = [ elem.target.location, elem ]; | ||
const main = elem._main || elem; | ||
if (!elem.$inferred && !main.$inferred) { | ||
info( 'assoc-target-not-in-service', loc, | ||
{ target, '#': (elem._main.query ? 'select' : 'define') }, { | ||
@@ -117,6 +119,9 @@ std: 'Target $(TARGET) of association is outside any service', // not used | ||
else { | ||
const text = main.$inferred === 'autoexposed' ? 'exposed' : 'std'; | ||
// ID published! Used in stakeholder project; if renamed, add to oldMessageIds | ||
info( 'assoc-outside-service', [ elem.target.location, elem ], | ||
{ target }, | ||
'Association target $(TARGET) is outside any service' ); | ||
info( 'assoc-outside-service', loc, { '#': text, target, service: main._service }, { | ||
std: 'Association target $(TARGET) is outside any service', | ||
// eslint-disable-next-line max-len | ||
exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service', | ||
} ); | ||
} | ||
@@ -265,2 +270,6 @@ } | ||
rewriteKeys( art, elem ); | ||
if (art.on) | ||
removeManagedPropsFromUnmanaged( art ); | ||
elem = art; | ||
@@ -270,2 +279,24 @@ } | ||
/** | ||
* Remove properties from unmanaged association `elem` that are only valid | ||
* on managed associations. Only set to `NULL` (special value for propagator), | ||
* if necessary, i.e. the value is set on the `_origin`-chain. | ||
*/ | ||
function removeManagedPropsFromUnmanaged( elem ) { | ||
removeProp( 'notNull' ); | ||
removeProp( 'default' ); | ||
function removeProp( prop ) { | ||
let origin = elem; | ||
while (origin) { | ||
if (origin[prop]) { // regardless of the value, reset the property | ||
const location = weakLocation( elem.name.location ); | ||
elem[prop] = { $inferred: 'NULL', val: undefined, location }; | ||
break; | ||
} | ||
origin = origin._origin; | ||
} | ||
} | ||
} | ||
function originTarget( elem ) { | ||
@@ -286,9 +317,9 @@ const assoc = !elem.expand && getOrigin( elem ); | ||
// 'Info','FK').toString()) | ||
elem.foreignKeys = Object.create(null); // set already here (also for zero foreign keys) | ||
forEachInOrder( assoc, 'foreignKeys', ( orig, name ) => { | ||
const fk = linkToOrigin( orig, name, elem, 'foreignKeys', elem.location ); | ||
const location = weakRefLocation( elem.target ); | ||
const fk = linkToOrigin( orig, name, elem, 'foreignKeys', location ); | ||
fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value? | ||
// TODO: re-check for case that foreign key is managed association | ||
if (orig._effectiveType !== undefined) | ||
setLink( fk, '_effectiveType', orig._effectiveType ); | ||
const te = copyExpr( orig.targetElement, elem.location ); | ||
setLink( fk, '_effectiveType', fk ); | ||
const te = copyExpr( orig.targetElement, location ); | ||
if (elem._redirected) { | ||
@@ -336,8 +367,5 @@ const i = te.path[0]; // TODO: or also follow path like for ON? | ||
// console.log(message( null, elem.location, elem, {art:assoc,target:assoc.target}, | ||
// 'Info','ON').toString(), nav) | ||
const { navigation } = nav; | ||
if (!navigation) // TODO: what about $projection.assoc as myAssoc ? | ||
return; // should not happen: $projection, $magic, or ref to const | ||
// console.log(message( null, elem.location, elem, {art:assoc}, 'Info','D').toString()) | ||
// Currently, having an unmanaged association inside a struct is not | ||
@@ -440,4 +468,9 @@ // supported by this function: | ||
// inside the same view (would otherwise be needed for mixins). | ||
if (elem.name.id.charAt(0) === '$') | ||
prependSelfToPath( expr.path, elem ); | ||
} | ||
} ); | ||
checkOnCondition( cond, 'on', elem ); | ||
return cond; | ||
@@ -478,2 +511,5 @@ } | ||
if (elem.name.id.charAt(0) === '$') | ||
prependSelfToPath( lhs.path, elem ); | ||
const rhs = { | ||
@@ -601,7 +637,8 @@ path: [ | ||
const { path } = ref; | ||
let root = path[0]; | ||
const root = path[0]; | ||
if (!elem) { | ||
if (location) { | ||
const elemref = root._navigation?.kind === '$self' ? path.slice(1) : path; | ||
error( 'rewrite-not-projected', [ location, assoc ], { | ||
name: assoc.name.id, art: item._artifact, elemref: { ref: path }, | ||
name: assoc.name.id, art: item._artifact, elemref: { ref: elemref }, | ||
}, { | ||
@@ -629,6 +666,3 @@ std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)', | ||
else if (elem.name.id.charAt(0) === '$') { | ||
root = { id: '$self', location: item.location }; | ||
path.unshift( root ); | ||
setLink( root, '_navigation', assoc._parent.$tableAliases.$self ); | ||
setArtifactLink( root, assoc._parent ); | ||
prependSelfToPath( path, assoc ); | ||
} | ||
@@ -660,2 +694,9 @@ else { | ||
function prependSelfToPath( path, elem ) { | ||
const root = { id: '$self', location: path[0].location }; | ||
path.unshift( root ); | ||
setLink( root, '_navigation', elem._parent.$tableAliases.$self ); | ||
setArtifactLink( root, elem._parent ); | ||
} | ||
function rewriteItem( elem, item, name, assoc, forKeys ) { | ||
@@ -662,0 +703,0 @@ if (!elem._redirected) |
@@ -14,2 +14,3 @@ // Simple compiler utility functions | ||
const { dictAdd, pushToDict, dictFirst } = require('../base/dictionaries'); | ||
const { weakLocation } = require('../base/location'); | ||
const { XsnName, XsnArtifact, CsnLocation } = require('./classes'); | ||
@@ -93,6 +94,7 @@ | ||
function linkToOrigin( origin, name, parent, prop, location, silentDep ) { | ||
location ||= weakLocation( origin.name.location ); // not ??= | ||
const elem = { | ||
name: { location: location || origin.name.location, id: name }, | ||
name: { location, id: origin.name.id }, | ||
kind: origin.kind, | ||
location: location || origin.location, | ||
location, | ||
}; | ||
@@ -106,2 +108,3 @@ if (origin.name.$inferred) | ||
// included elements? (Currently for $inferred: 'expanded' only) | ||
// TODO: shouldn't we always use silent dependencies in this function? | ||
if (silentDep) | ||
@@ -247,7 +250,7 @@ dependsOnSilent( elem, origin ); | ||
function copyExpr( expr, location, skipUnderscored, rewritePath ) { | ||
function copyExpr( expr, location ) { | ||
if (!expr || typeof expr !== 'object') | ||
return expr; | ||
else if (Array.isArray( expr )) | ||
return expr.map( e => copyExpr( e, location, skipUnderscored, rewritePath ) ); | ||
return expr.map( e => copyExpr( e, location ) ); | ||
@@ -262,19 +265,13 @@ const proto = Object.getPrototypeOf( expr ); | ||
const pd = Object.getOwnPropertyDescriptor( expr, prop ); | ||
if (!pd.enumerable) { // should include all properties starting with _ | ||
if (!skipUnderscored || | ||
prop === '_artifact' || prop === '_navigation' || prop === '_effectiveType') | ||
Object.defineProperty( r, prop, pd ); | ||
} | ||
else if (!proto) { | ||
r[prop] = copyExpr( pd.value, location, skipUnderscored, rewritePath ); | ||
} | ||
else if (prop === 'location') { | ||
if (!proto) | ||
r[prop] = copyExpr( pd.value, location ); | ||
else if (!pd.enumerable || prop.charAt(0) === '$') | ||
Object.defineProperty( r, prop, pd ); | ||
else if (prop === 'location') | ||
r[prop] = location || pd.value; | ||
} | ||
else if (prop.charAt(0) !== '$' || prop === '$inferred') { | ||
r[prop] = copyExpr( pd.value, location, skipUnderscored, rewritePath ); | ||
} | ||
else if (!skipUnderscored) { // skip $ properties | ||
Object.defineProperty( r, prop, pd ); | ||
} | ||
else | ||
r[prop] = copyExpr( pd.value, location ); | ||
} | ||
@@ -566,6 +563,24 @@ return r; | ||
function isDirectComposition( art ) { | ||
const type = art.type && art.type.path; | ||
return type && type[0] && type[0].id === 'cds.Composition'; | ||
const path = art.type?.path; | ||
return path?.length === 1 && path[0].id === 'cds.Composition'; | ||
} | ||
function targetCantBeAspect( elem, calledForTargetAspectProp ) { | ||
// Remark: we do not check `on` and `keys` here - the error should complain | ||
// at the `on`/`keys`, not the aspect | ||
if (!isDirectComposition( elem ) || elem.targetAspect && !calledForTargetAspectProp) | ||
return (elem.type && !elem.type.$inferred) ? 'std' : 'redirected'; | ||
if (!elem._main) | ||
return elem.kind; // type or annotation | ||
// TODO: extra for "in many"? | ||
let art = elem; | ||
while (art.kind === 'element') | ||
art = art._parent; | ||
if (![ 'entity', 'aspect', 'event' ].includes( art.kind )) | ||
return (art.kind !== 'mixin') ? art.kind : 'select'; | ||
return ((art.query || art.kind === 'event') && !(calledForTargetAspectProp && elem.target)) | ||
? art.kind | ||
: elem._parent.kind === 'element' && 'sub'; | ||
} | ||
function userQuery( user ) { | ||
@@ -657,3 +672,4 @@ // TODO: we need _query links set by the definer | ||
function compositionTextVariant( art, composition, association = 'std' ) { | ||
return (getUnderlyingBuiltinType( art )?.name.absolute === 'cds.Composition') | ||
const builtin = getUnderlyingBuiltinType( art ); | ||
return (!builtin._main && builtin.name.id === 'cds.Composition') | ||
? composition | ||
@@ -693,2 +709,3 @@ : association; | ||
isDirectComposition, | ||
targetCantBeAspect, | ||
userQuery, | ||
@@ -695,0 +712,0 @@ pathStartsWithSelf, |
@@ -8,2 +8,4 @@ { | ||
"rules": { | ||
"cds-compiler/message-no-quotes": "off", | ||
"cds-compiler/message-template-string": "off", | ||
"prefer-const": "error", | ||
@@ -10,0 +12,0 @@ "quotes": ["error", "single", "avoid-escape"], |
@@ -13,8 +13,13 @@ 'use strict'; | ||
const { setProp, isBetaEnabled } = require('../base/model'); | ||
const { cloneCsnNonDict, isEdmPropertyRendered, isBuiltinType } = require('../model/csnUtils'); | ||
const { | ||
cloneCsnNonDict, isEdmPropertyRendered, isBuiltinType, getUtils, | ||
} = require('../model/csnUtils'); | ||
const { checkCSNVersion } = require('../json/csnVersion'); | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { | ||
EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm, | ||
} = require('./edm.js'); | ||
EdmTypeFacetMap, | ||
EdmTypeFacetNames, | ||
EdmPrimitiveTypeMap, | ||
} = require('./EdmPrimitiveTypeDefinitions.js'); | ||
const { getEdm } = require('./edm.js'); | ||
@@ -753,3 +758,3 @@ /* | ||
const definition = schemaCsn.definitions[rt]; | ||
if (definition && definition.kind === 'entity' && !definition.abstract) | ||
if (definition && definition.kind === 'entity' && !definition.abstract && !edmUtils.isSingleton(definition)) | ||
actionImport.setEdmAttribute('EntitySet', edmUtils.getBaseName(rt)); | ||
@@ -1051,2 +1056,4 @@ } | ||
function addAnnotations2XServiceRefs( ) { | ||
const { getFinalTypeInfo } = getUtils(csn); | ||
options.getFinalTypeInfo = getFinalTypeInfo; | ||
const { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions, mergedVocabularies); | ||
@@ -1120,3 +1127,3 @@ // distribute edm:Annotations into the schemas | ||
{ | ||
type: edmType, number: scale, rawvalue: precision, ersion: (p.v4 ? '4.0' : '2.0'), '#': 'scale', | ||
type: edmType, number: scale, rawvalue: precision, '#': 'scale', | ||
}); | ||
@@ -1123,0 +1130,0 @@ } |
@@ -6,71 +6,8 @@ 'use strict'; | ||
const { forEach } = require('../utils/objectUtils'); | ||
const { | ||
EdmTypeFacetMap, | ||
EdmTypeFacetNames, | ||
EdmPrimitiveTypeMap, | ||
} = require('./EdmPrimitiveTypeDefinitions.js'); | ||
// facet definitions, optional could either be true or array of edm types | ||
// remove indicates wether or not the canonic facet shall be removed when applying @odata.Type | ||
const EdmTypeFacetMap = { | ||
MaxLength: { | ||
v2: true, v4: true, remove: true, optional: true, | ||
}, | ||
Precision: { | ||
v2: true, v4: true, remove: true, optional: true, | ||
}, | ||
Scale: { | ||
v2: true, v4: true, remove: true, optional: true, extra: 'sap:variable-scale', | ||
}, | ||
SRID: { v4: true, remove: true, optional: true }, | ||
// 'FixedLength': { v2: true }, | ||
// 'Collation': { v2: true }, | ||
// 'Unicode': { v2: true, v4: true }, | ||
}; | ||
const EdmTypeFacetNames = Object.keys(EdmTypeFacetMap); | ||
// Merged primitive type map with descriptions taken from V4 spec and filled up with V2 spec | ||
const EdmPrimitiveTypeMap = { | ||
'Edm.Binary': { | ||
v2: true, v4: true, MaxLength: true, FixedLength: true, desc: 'Binary data', | ||
}, | ||
'Edm.Boolean': { v2: true, v4: true, desc: 'Binary-valued logic' }, | ||
'Edm.Byte': { v2: true, v4: true, desc: 'Unsigned 8-bit integer' }, | ||
'Edm.Date': { v4: true, desc: 'Date without a time-zone offset' }, | ||
'Edm.DateTime': { v2: true, Precision: true, desc: 'Date and time with values ranging from 12:00:00 midnight, January 1, 1753 A.D. through 11:59:59 P.M, December 31, 9999 A.D.' }, | ||
'Edm.DateTimeOffset': { | ||
v2: true, v4: true, Precision: true, desc: 'Date and time with a time-zone offset, no leap seconds', | ||
}, | ||
'Edm.Decimal': { | ||
v2: true, v4: true, Precision: true, Scale: true, desc: 'Numeric values with decimal representation', | ||
}, | ||
'Edm.Double': { v2: true, v4: true, desc: 'IEEE 754 binary64 floating-point number (15-17 decimal digits)' }, | ||
'Edm.Duration': { v4: true, Precision: true, desc: 'Signed duration in days, hours, minutes, and (sub)seconds' }, | ||
'Edm.Guid': { v2: true, v4: true, desc: '16-byte (128-bit) unique identifier' }, | ||
'Edm.Int16': { v2: true, v4: true, desc: 'Signed 16-bit integer' }, | ||
'Edm.Int32': { v2: true, v4: true, desc: 'Signed 32-bit integer' }, | ||
'Edm.Int64': { v2: true, v4: true, desc: 'Signed 64-bit integer' }, | ||
'Edm.SByte': { v2: true, v4: true, desc: 'Signed 8-bit integer' }, | ||
'Edm.Single': { v2: true, v4: true, desc: 'IEEE 754 binary32 floating-point number (6-9 decimal digits)' }, | ||
'Edm.Stream': { v4: true, MaxLength: true, desc: 'Binary data stream' }, | ||
'Edm.String': { | ||
v2: true, v4: true, MaxLength: true, FixedLength: true, Collation: true, Unicode: true, desc: 'Sequence of characters', | ||
}, | ||
'Edm.Time': { v2: true, Precision: true, desc: 'time of day with values ranging from 0:00:00.x to 23:59:59.y, where x and y depend upon the precision' }, | ||
'Edm.TimeOfDay': { v4: true, Precision: true, desc: 'Clock time 00:00-23:59:59.999999999999' }, | ||
'Edm.Geography': { v4: true, SRID: true, desc: 'Abstract base type for all Geography types' }, | ||
'Edm.GeographyPoint': { v4: true, SRID: true, desc: 'A point in a round-earth coordinate system' }, | ||
'Edm.GeographyLineString': { v4: true, SRID: true, desc: 'Line string in a round-earth coordinate system' }, | ||
'Edm.GeographyPolygon': { v4: true, SRID: true, desc: 'Polygon in a round-earth coordinate system' }, | ||
'Edm.GeographyMultiPoint': { v4: true, SRID: true, desc: 'Collection of points in a round-earth coordinate system' }, | ||
'Edm.GeographyMultiLineString': { v4: true, SRID: true, desc: 'Collection of line strings in a round-earth coordinate system' }, | ||
'Edm.GeographyMultiPolygon': { v4: true, SRID: true, desc: 'Collection of polygons in a round-earth coordinate system' }, | ||
'Edm.GeographyCollection': { v4: true, SRID: true, desc: 'Collection of arbitrary Geography values' }, | ||
'Edm.Geometry': { v4: true, SRID: true, desc: 'Abstract base type for all Geometry types' }, | ||
'Edm.GeometryPoint': { v4: true, SRID: true, desc: 'Point in a flat-earth coordinate system' }, | ||
'Edm.GeometryLineString': { v4: true, SRID: true, desc: 'Line string in a flat-earth coordinate system' }, | ||
'Edm.GeometryPolygon': { v4: true, SRID: true, descr: 'Polygon in a flat-earth coordinate system' }, | ||
'Edm.GeometryMultiPoint': { v4: true, SRID: true, desc: 'Collection of points in a flat-earth coordinate system' }, | ||
'Edm.GeometryMultiLineString': { v4: true, SRID: true, desc: 'Collection of line strings in a flat-earth coordinate system' }, | ||
'Edm.GeometryMultiPolygon': { v4: true, SRID: true, desc: 'Collection of polygons in a flat-earth coordinate system' }, | ||
'Edm.GeometryCollection': { v4: true, SRID: true, desc: 'Collection of arbitrary Geometry values' }, | ||
'Edm.PrimitiveType': { v4: true, desc: 'Abstract meta type' }, | ||
// 'Edm.Untyped': { v4: true, desc: 'Abstract void type' }, | ||
}; | ||
function getEdm( options, messageFunctions ) { | ||
@@ -1448,6 +1385,10 @@ const { error } = messageFunctions || { error: () => true, warning: () => true }; | ||
toXMLattributes() { | ||
// TODO: Why json? | ||
if (this._jsonOnlyAttributes.Collection) | ||
return ` Type="Collection(${this._edmAttributes.Type})"`; | ||
return ` Type="${this._edmAttributes.Type}"`; | ||
if (this._jsonOnlyAttributes.Collection) { | ||
const ot = this._edmAttributes.Type; | ||
this._edmAttributes.Type = `Collection(${ot})`; | ||
const str = super.toXMLattributes(); | ||
this._edmAttributes.Type = ot; | ||
return str; | ||
} | ||
return super.toXMLattributes(); | ||
} | ||
@@ -1632,3 +1573,3 @@ toJSON() { | ||
module.exports = { | ||
EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm, | ||
getEdm, | ||
}; |
@@ -48,3 +48,4 @@ // Transform XSN (augmented CSN) into CSN | ||
'localized-entity': 'localized', | ||
localized: 'localized', // on elements (texts, localized) | ||
localized: 'localized', // on elements (texts, localized, language) | ||
// remark: not on 'localize-origin' = other elements of inferred base entity | ||
'composition-entity': 'composed', // ('aspect-composition' on element not in CSN) | ||
@@ -679,5 +680,5 @@ }; | ||
function location( loc, csn, xsn ) { | ||
if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' && | ||
(!xsn.$inferred || !xsn._main)) { // TODO: also for 'select' | ||
// Also include $location for elements in queries (if not via '*') | ||
if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' && // TODO: also for 'select' | ||
(!xsn.$inferred || !xsn._main)) { | ||
// Also include $location for elements in queries (if not via '*' except for autoexposed) | ||
addLocation( xsn.name && xsn.name.location || loc, csn ); | ||
@@ -773,3 +774,4 @@ } | ||
art.name, neqPath( art.targetElement ) ); | ||
addLocation( art.targetElement.location, key ); | ||
if (!art.$inferred) | ||
addLocation( art.targetElement.location, key ); | ||
return extra( key, art ); | ||
@@ -1197,4 +1199,5 @@ } | ||
if (universalCsn && node.$inferred) { | ||
// TODO: return undefined for all values of node.$inferred (except 'NULL')? | ||
if (node.$inferred === 'prop' || node.$inferred === '$generated' || // via propagator.js | ||
node.$inferred === 'parent-origin') | ||
node.$inferred === 'parent-origin') | ||
return undefined; | ||
@@ -1257,2 +1260,4 @@ else if (node.$inferred === 'NULL') | ||
function expression( node ) { | ||
if (node?.$inferred && (gensrcFlavor || universalCsn || node.$inferred === 'NULL')) | ||
return undefined; // Note: No `null` for universal CSN at the moment | ||
const expr = exprInternal( node, 'no' ); | ||
@@ -1259,0 +1264,0 @@ return (Array.isArray( expr )) |
@@ -129,3 +129,2 @@ // @ts-nocheck : Issues with Tokens on `this`, e.g. `this.DOT`. | ||
parser._errHandler = new errorStrategy.KeywordErrorStrategy(); | ||
parser.match = errorStrategy.match; | ||
parser._interp.predictionMode = antlr4.atn.PredictionMode.SLL; | ||
@@ -132,0 +131,0 @@ // parser._interp.predictionMode = antlr4.atn.PredictionMode.LL_EXACT_AMBIG_DETECTION; |
@@ -42,3 +42,3 @@ 'use strict'; | ||
.replace(/^\/[*]{2,}/, '') | ||
.replace(/\*+\/$/, '') | ||
.replace(/\**\/$/, '') // for `/*****/`, only `/` remains | ||
.replace('*\\/', '*/') // escape sequence | ||
@@ -45,0 +45,0 @@ .trim(); |
@@ -48,23 +48,2 @@ // Error strategy with special handling for (non-reserved) keywords | ||
// Match current token against token type `ttype` and consume it if successful. | ||
// Also allow to match keywords as identifiers. This function should be set as | ||
// property `match` to the parser (prototype). See also `recoverInline()`. | ||
function match( ttype ) { | ||
const identType = this.constructor.Identifier; | ||
if (ttype !== identType) | ||
return antlr4.Parser.prototype.match.call( this, ttype ); | ||
const token = this.getCurrentToken(); | ||
if (token.type === identType || !keywordRegexp.test( token.text )) | ||
return antlr4.Parser.prototype.match.call( this, ttype ); | ||
// This is very likely to be dead code: we do not use a simple Identifier | ||
// without alternatives in the grammar. With alternatives, recoverInline() is | ||
// the place to go. (But this code should work with a changed grammar…) | ||
this.message( 'syntax-unexpected-reserved-word', token, | ||
{ code: token.text, delimited: token.text } ); | ||
this._errHandler.reportMatch(this); | ||
this.consume(); | ||
return token; | ||
} | ||
// Class which adapts ANTLR4s standard error strategy: do something special | ||
@@ -89,2 +68,3 @@ // with (non-reserved) keywords. | ||
sync, | ||
singleTokenDeletion, | ||
reportNoViableAlternative, | ||
@@ -107,2 +87,3 @@ reportInputMismatch, | ||
// TODO: consider performance - see #8800 | ||
// See DefaultErrorStrategy#sync | ||
function sync( recognizer ) { | ||
@@ -182,2 +163,4 @@ // If already recovering, don't try to sync | ||
} | ||
// TODO: at least with STAR_LOOP_ENTRY, we might want to do s/th similar as | ||
// with LOOP_BACK (syncing to “expected tokens” -> the separator) | ||
throw new InputMismatchException(recognizer); | ||
@@ -191,5 +174,24 @@ | ||
expecting.addSet(recognizer.getExpectedTokens()); | ||
// First try some ',' insertion (TODO does not work yet): | ||
if (trySeparatorInsertion( recognizer, expecting, "','" )) | ||
return; | ||
// We then try syncing only to the loop-cont (`,`) / loop-end (`}`) token set, | ||
// but only for the current or next line (and not consuming `;`s): | ||
const prevToken = recognizer.getTokenStream().LT(-1); | ||
if (token.line <= prevToken.line + 1 && // in same or next line | ||
this.consumeAndMarkUntil( recognizer, expecting, true )) | ||
break; | ||
// console.log(token.text,JSON.stringify(intervalSetToArray(recognizer,expecting))) | ||
// If that fails, we also sync to all tokens which are in the follow set of | ||
// the current rule and all outer rules | ||
const whatFollowsLoopIterationOrRule = expecting.addSet(this.getErrorRecoverySet(recognizer)); | ||
this.consumeUntil(recognizer, whatFollowsLoopIterationOrRule); | ||
break; | ||
// console.log(JSON.stringify(intervalSetToArray(recognizer,expecting))) | ||
if (recognizer._ctx._sync === 'recover' || // in start rule: no exception | ||
nextTokens.contains( recognizer.getTokenStream().LA(1) )) | ||
return; | ||
throw new InputMismatchException(recognizer); | ||
} | ||
@@ -201,2 +203,61 @@ default: | ||
function trySeparatorInsertion( recognizer, expecting, separatorName ) { | ||
// Remark: this function does not really work, because it is based on | ||
// singleTokenInsertion, which also does not really work… (see below). | ||
// But we might improve it in the future… | ||
const separator = recognizer.literalNames.indexOf( separatorName ); | ||
if (!expecting.contains( separator )) | ||
return false; | ||
const currentSymbolType = recognizer.getTokenStream().LA(1); | ||
// if current token is consistent with what could come after current | ||
// ATN state, then we know we're missing a token; error recovery | ||
// is free to conjure up and insert the missing token | ||
const { atn } = recognizer._interp; | ||
const currentState = atn.states[recognizer.state]; | ||
const next = separatorTransition( currentState.transitions, separator ).target; | ||
// While this is an improvement to the default ANTLR code for | ||
// singleTokenInsertion(), it still does not help, as we navigate along an | ||
// epsilon transition, i.e. we still see ',', etc | ||
const expectingAtLL2 = atn.nextTokens(next, recognizer._ctx); | ||
if (!expectingAtLL2.contains(currentSymbolType)) | ||
return false; | ||
this.reportMissingToken(recognizer); | ||
return getMissingSymbol( recognizer, separator ); | ||
} | ||
function separatorTransition( transitions, separator ) { | ||
for (const tr of transitions) { | ||
if (tr.matches( separator )) | ||
return tr; | ||
} | ||
return transitions[0]; | ||
} | ||
function singleTokenDeletion( recognizer ) { | ||
const token = recognizer.getCurrentToken(); | ||
if (!token || token.text === '}') | ||
return null; | ||
const nextTokenType = recognizer.getTokenStream().LA(2); | ||
const { Number } = recognizer.constructor; | ||
if (nextTokenType > Number && // next token is Id|Unreserved|IllegalToken | ||
token.type <= Number) // current token is not | ||
return null; | ||
const expecting = this.getExpectedTokens(recognizer); | ||
if (!expecting.contains(nextTokenType)) | ||
return null; | ||
this.reportUnwantedToken(recognizer); | ||
recognizer.consume(); // simply delete extra token | ||
// we want to return the token we're actually matching | ||
const matchedSymbol = recognizer.getCurrentToken(); | ||
this.reportMatch( recognizer ); // we know current token is correct | ||
return matchedSymbol; | ||
} | ||
// singleTokenInsertion called by recoverInline (called by match / in else), | ||
@@ -243,3 +304,3 @@ // calls reportMissingToken | ||
// Report unwanted token when the parser `recognizer` tries to recover/sync | ||
function reportUnwantedToken( recognizer ) { | ||
function reportUnwantedToken( recognizer, expecting ) { | ||
if (this.inErrorRecoveryMode(recognizer)) | ||
@@ -251,3 +312,3 @@ return; | ||
token.$isSkipped = 'offending'; | ||
const expecting = this.getExpectedTokensForMessage( recognizer, token ); | ||
expecting ??= this.getExpectedTokensForMessage( recognizer, token ); | ||
const offending = this.getTokenDisplay( token, recognizer ); | ||
@@ -334,10 +395,15 @@ // Just text variant, no other message id! Would depend on ANTLR-internals | ||
function consumeAndMarkUntil( recognizer, set ) { | ||
let t = recognizer.getTokenStream().LT(1); | ||
function consumeAndMarkUntil( recognizer, set, onlyInSameLine ) { | ||
const stream = recognizer.getTokenStream(); | ||
let t = stream.LT(1); | ||
const { line } = t; | ||
while (t.type !== antlr4.Token.EOF && !set.contains( t.type )) { | ||
if (onlyInSameLine && (t.line !== line || t.text === ';' || t.text === '}' )) | ||
return false; // early exit | ||
if (!t.$isSkipped) | ||
t.$isSkipped = true; | ||
recognizer.consume(); | ||
t = recognizer.getTokenStream().LT(1); | ||
t = stream.LT(1); | ||
} | ||
return true; | ||
} | ||
@@ -375,4 +441,4 @@ | ||
// Think about: we might want to prefer one of '}]);,'. | ||
function getMissingSymbol( recognizer ) { | ||
const expectedTokenType = this.getExpectedTokens(recognizer).first(); // get any element | ||
function getMissingSymbol( recognizer, expectedTokenType ) { | ||
expectedTokenType ??= this.getExpectedTokens(recognizer).first(); // get any element | ||
const current = recognizer.getCurrentToken(); | ||
@@ -569,4 +635,3 @@ return recognizer.getTokenFactory().create( | ||
module.exports = { | ||
match, | ||
KeywordErrorStrategy, | ||
}; |
@@ -133,2 +133,3 @@ // Generic ANTLR parser class with AST-building functions | ||
csnParseOnly, | ||
markAsSkippedUntilEOF, | ||
noAssignmentInSameLine, | ||
@@ -253,2 +254,21 @@ noSemicolonHere, | ||
function markAsSkippedUntilEOF() { | ||
let t = this.getCurrentToken(); | ||
if (t.type === antlr4.Token.EOF) | ||
return; | ||
if (!t.$isSkipped && !this._errHandler.inErrorRecoveryMode( this )) { | ||
// If not already done, we should report an error if we do not see EOF. We cannot | ||
// use match() here, because these would consume tokens without marking them. | ||
this._errHandler.reportUnwantedToken( this, [ '<EOF>' ] ); | ||
t.$isSkipped = 'offending'; | ||
this.consume(); | ||
t = this.getCurrentToken(); | ||
} | ||
while (t.type !== antlr4.Token.EOF) { | ||
t.$isSkipped = true; | ||
this.consume(); | ||
t = this.getCurrentToken(); | ||
} | ||
} | ||
function noAssignmentInSameLine() { | ||
@@ -933,3 +953,2 @@ const t = this.getCurrentToken(); | ||
if (sign) { | ||
// TODO: warning for space in between | ||
const { endLine, endCol } = location; | ||
@@ -940,2 +959,3 @@ location = this.startLocation( sign ); | ||
text = sign.text + text; | ||
this.reportUnexpectedSpace( sign, this.tokenLocation( token ) ); | ||
} | ||
@@ -942,0 +962,0 @@ |
@@ -16,2 +16,3 @@ // Main entry point for the CDS Compiler | ||
const { traceApi } = require('./api/trace'); | ||
const snapi = lazyload('./api/main'); | ||
@@ -79,5 +80,14 @@ const csnUtils = lazyload('./model/csnUtils'); | ||
version, | ||
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 | ||
compile: (filenames, dir, options, fileCache) => { // main function | ||
traceApi( 'compile', options ); | ||
return compiler.compileX(filenames, dir, options, fileCache).then(toCsn.compactModel); | ||
}, | ||
compileSync: (filenames, dir, options, fileCache) => { // main function | ||
traceApi('compileSync', options); | ||
return toCsn.compactModel(compiler.compileSyncX(filenames, dir, options, fileCache)); | ||
}, | ||
compileSources: (sourcesDict, options) => { // main function | ||
traceApi('compileSources', options); | ||
return toCsn.compactModel(compiler.compileSourcesX(sourcesDict, options)); | ||
}, | ||
compactModel: csn => csn, // for easy v2 migration | ||
@@ -84,0 +94,0 @@ get CompilationError() { |
@@ -197,2 +197,3 @@ // CSN functionality for resolving references | ||
const { ModelError, CompilerAssertion } = require('../base/error'); | ||
const { isAnnotationExpression } = require('../compiler/builtins'); | ||
@@ -225,2 +226,4 @@ // Properties in which artifact or members are defined - next property in the | ||
on: { lexical: justDollar, dynamic: 'query' }, // assoc defs, redirected to | ||
annotation: { lexical: justDollar, dynamic: 'query' }, // anno top-level `ref` | ||
annotationExpr: { lexical: justDollar, dynamic: 'query' }, // annotation assignment | ||
// there are also 'on_join' and 'on_mixin' with default semantics | ||
@@ -472,2 +475,12 @@ orderBy_ref: { lexical: query => query, dynamic: 'query' }, | ||
function boundActionOrMain( art ) { | ||
while (art.kind !== 'action' && art.kind !== 'function') { | ||
const p = getCache( art, '_parent' ); | ||
if (!p) | ||
return art; | ||
art = p; | ||
} | ||
return art; | ||
} | ||
function initDefinition( main ) { | ||
@@ -578,9 +591,12 @@ // TODO: some --test-mode check that the argument is in ‹csn›.definitions ? | ||
throw new ModelError( 'References must look like {ref:[...]}' ); | ||
const head = pathId( path[0] ); | ||
if (main) // TODO: improve, for csnpath starting with art | ||
initDefinition( main ); | ||
if (ref.param) | ||
return resolvePath( path, main.params[head], main, 'param' ); | ||
const head = pathId( path[0] ); | ||
if (ref.param) { | ||
const boundOrMain = (query || !main.actions || parent === main) | ||
? main // shortcut (would also have been return by function) | ||
: boundActionOrMain( parent ); | ||
return resolvePath( path, boundOrMain.params[head], boundOrMain, 'param' ); | ||
} | ||
const semantics = referenceSemantics[refCtx] || {}; | ||
@@ -651,2 +667,3 @@ if (semantics.$initOnly) | ||
// TODO: for ON condition in expand, would need to use cached _element | ||
// TODO: test and implement - Issue #11792! | ||
return resolvePath( path, qcache.elements[head], null, 'query' ); | ||
@@ -1010,3 +1027,3 @@ } | ||
return { | ||
index: 2, main: art, parent: null, art, | ||
index: 2, main: art, parent: art, art, | ||
}; | ||
@@ -1042,6 +1059,19 @@ } | ||
const prop = csnPath[index]; | ||
if (refCtx === 'annotation' && typeof obj === 'object') { | ||
// we do not know yet whether the annotation value is a expression or not → | ||
// loop over outer array and records (structure values): | ||
if (Array.isArray( obj ) || !isAnnotationExpression( obj )) { | ||
obj = obj[prop]; | ||
continue; | ||
} | ||
refCtx = 'annotationExpr'; | ||
} | ||
// array item, name/index of artifact/member, (named) argument | ||
if (isName || Array.isArray( obj ) || prop === 'returns') { | ||
// TODO: call some kind of resolve.setOrigin() | ||
if (typeof isName === 'string' || prop === 'returns') { | ||
if (isName === 'actions') { | ||
art = obj[prop]; | ||
parent = art; // param refs in annos for actions are based on the action, not the entity | ||
} | ||
else if (typeof isName === 'string' || prop === 'returns') { | ||
parent = art; | ||
@@ -1068,3 +1098,3 @@ art = obj[prop]; | ||
art = obj; | ||
parent = null; | ||
parent = obj; | ||
} | ||
@@ -1117,3 +1147,7 @@ isName = prop; | ||
} | ||
else if (prop !== 'xpr') { | ||
else if (prop.charAt(0) === '@') { | ||
refCtx = 'annotation'; | ||
} | ||
else if (prop !== 'xpr' && prop !== 'list') { | ||
// 'xpr' and 'list' do not change the ref context, all other props do: | ||
refCtx = prop; | ||
@@ -1120,0 +1154,0 @@ } |
@@ -1165,3 +1165,3 @@ 'use strict'; | ||
* @param {CSN.Model} csn | ||
* @param {{notFound?: (name: string, index: number) => void, override?: boolean, filter?: (name: string) => boolean}} config | ||
* @param {{notFound?: (name: string, index: number) => void, override?: boolean, filter?: (name: string) => boolean, applyToElements?: boolean}} config | ||
* notFound: Function that is called if the referenced definition can't be found. | ||
@@ -1172,2 +1172,3 @@ * Second argument is index in `csn.extensions` array. | ||
* will be applied. | ||
* applyToElements: Wether to apply annotations to elements or only to artifacts | ||
*/ | ||
@@ -1179,2 +1180,3 @@ function applyAnnotationsFromExtensions( csn, config ) { | ||
const filter = config.filter || (_name => true); | ||
const applyToElements = config.applyToElements ?? true; | ||
for (let i = 0; i < csn.extensions.length; ++i) { | ||
@@ -1187,3 +1189,6 @@ const ext = csn.extensions[i]; | ||
moveAnnotationsAndDoc(ext, def, config.override); | ||
applyAnnotationsToElements(ext, def); | ||
if (applyToElements) | ||
applyAnnotationsToElements(ext, def); | ||
if (Object.keys(ext).length <= 1) | ||
csn.extensions[i] = undefined; | ||
} | ||
@@ -1196,2 +1201,4 @@ else if (config.notFound) { | ||
csn.extensions = csn.extensions.filter(ext => ext); | ||
function applyAnnotationsToElements( ext, def ) { | ||
@@ -1212,4 +1219,9 @@ // Only the definition is arrayed but the extension is not since | ||
applyAnnotationsToElements(sourceElem, targetElem); | ||
if (Object.keys(sourceElem).length === 0) | ||
delete ext.elements[key]; | ||
} | ||
}); | ||
if (Object.keys(ext.elements).length === 0) | ||
delete ext.elements; | ||
} | ||
@@ -1216,0 +1228,0 @@ } |
@@ -45,2 +45,3 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs | ||
const { CompilerAssertion } = require('../base/error'); | ||
const { isAnnotationExpression } = require('../compiler/builtins'); | ||
const shuffleGen = require('../base/shuffle'); | ||
@@ -65,3 +66,4 @@ | ||
// TODO: excluding | ||
'@': () => { /* ignore annotations */ }, | ||
'@': assignment, | ||
$: () => { /* ignore properties like $location for performance */ }, | ||
}; | ||
@@ -136,5 +138,26 @@ // options.enrichCsn = 'DEBUG'; | ||
function assignment( parent, prop, obj ) { | ||
if (!obj || typeof obj !== 'object' || !{}.propertyIsEnumerable.call( parent, prop )) | ||
return; | ||
csnPath.push( prop ); | ||
if (Array.isArray(obj)) { | ||
obj.forEach( (n, i) => assignment( obj, i, n ) ); | ||
} | ||
else { | ||
const record = !isAnnotationExpression( obj ) && assignment; | ||
// is record without `=` and other expression property | ||
for (const name of Object.getOwnPropertyNames( obj ) ) { | ||
const trans = record || transformers[name] || transformers[name.charAt(0)] || standard; | ||
trans( obj, name, obj[name] ); | ||
} | ||
if (!record && obj.$parens) | ||
reveal( obj, '$parens', obj.$parens ); | ||
} | ||
csnPath.pop(); | ||
} | ||
function refLocation( art ) { | ||
if (!art || typeof art !== 'object' || Array.isArray( art )) { | ||
if (!options.testMode) { | ||
if (catchRefError()) { | ||
return (typeof art === 'string') | ||
@@ -150,3 +173,3 @@ ? `<illegal ref = ${ art }>` | ||
if (!options.testMode) | ||
if (catchRefError()) | ||
return `<${ Object.keys( art ).join('+') }+!$location>`; | ||
@@ -158,3 +181,3 @@ throw new CompilerAssertion( 'Reference to object without $location' ); | ||
// try { | ||
const notFound = (options.testMode) ? undefined : null; | ||
const notFound = (catchRefError()) ? null : undefined; | ||
if (Array.isArray( ref )) { | ||
@@ -228,3 +251,3 @@ parent[`_${ prop }`] = ref.map( r => refLocation( artifactRef( r, notFound ) ) ); | ||
function handleError( callback ) { | ||
if (options.testMode) | ||
if (!catchRefError()) | ||
return callback(); | ||
@@ -344,2 +367,7 @@ try { | ||
} | ||
function catchRefError() { | ||
return !options.testMode || // false && | ||
csnPath.some( p => typeof p === 'string' && p.charAt(0) === '@'); | ||
} | ||
} | ||
@@ -346,0 +374,0 @@ |
@@ -107,2 +107,4 @@ // Make internal properties of the XSN / augmented CSN visible | ||
$builtins: nameOrPath === '++' ? builtinsDictionary : () => '‹reveal with -R ++›', | ||
tokenStream: ts => `‹${ ts?.tokens?.length ?? '?' } tokens›`, | ||
parseListener: _ => '‹parseListener›', | ||
}; | ||
@@ -296,2 +298,3 @@ uniqueId = -1; | ||
function targetAspect( node, parent ) { | ||
// TODO: avoid repeated display of same target aspect (includes) | ||
if (node.elements && node.__unique_id__ == null && node.$effectiveSeqNo == null) | ||
@@ -344,2 +347,4 @@ Object.defineProperty( node, '__unique_id__', { value: uniqueId-- } ); | ||
case undefined: | ||
if (node.name.id === '$self' && node.location.file === '') | ||
return `type:${ quoted( '$self' ) }##0`; | ||
return (node._artifact && node._artifact.kind) | ||
@@ -346,0 +351,0 @@ ? artifactIdentifier( node._artifact ) |
@@ -10,4 +10,3 @@ 'use strict'; | ||
} = require('../model/csnUtils'); | ||
const { isBetaEnabled } = require('../base/model'); | ||
const { forEachKey } = require('../utils/objectUtils'); | ||
const { forEachKey, forEach } = require('../utils/objectUtils'); | ||
@@ -37,2 +36,3 @@ // used to mark a view as changed so we know to drop-create it | ||
returnObj.unchangedConstraints = new Set(); | ||
returnObj.changedPrimaryKeys = []; | ||
@@ -79,17 +79,39 @@ // There is currently no use in knowing the added entities only. If this changes, hand in `addedEntities` to `getArtifactComparator` below. | ||
*/ | ||
function getExtensionAndMigrations(beforeModel, options, { extensions, migrations, unchangedConstraints }) { | ||
function getExtensionAndMigrations(beforeModel, options, { extensions, migrations, unchangedConstraints, changedPrimaryKeys }) { | ||
return function compareArtifacts(artifact, name) { | ||
let hasPrimaryKeyChange = false; | ||
function addElements() { | ||
const elements = {}; | ||
forEachMember(artifact, getElementComparator(otherArtifact, elements), [ 'definitions', name ], true, { elementsOnly: true }); | ||
const keysNow = []; | ||
forEachMember(artifact, (element, eName) => { | ||
getElementComparator(otherArtifact, elements)(element, eName); | ||
if(element.key) | ||
keysNow.push(eName); | ||
}, [ 'definitions', name ], true, { elementsOnly: true }); | ||
// Only do this check for to.hdi.migration - the order only "bites" us when doing .hdbmigrationtable as the end-check against the intended | ||
// create-table will fail. TODO: Does a mismatched order of the primary key hurt us for postgres and others? | ||
if(!hasPrimaryKeyChange && options.sqlDialect === 'hana' && options.src === 'hdi') { | ||
const keysOther = []; | ||
forEachMember(otherArtifact, (element, eName) => { | ||
if(element.key) | ||
keysOther.push(eName); | ||
}, [ 'definitions', name ], true, { elementsOnly: true }); | ||
if(keysNow.join(',') !== keysOther.join(',')) | ||
hasPrimaryKeyChange = true; | ||
} | ||
if (Object.keys(elements).length > 0) { | ||
extensions.push(addedElements(name, elements)); | ||
const added = addedElements(name, elements); | ||
if(!hasPrimaryKeyChange) | ||
forEach(added.elements, (_name, element) => { | ||
if(element.key && !element.target) | ||
hasPrimaryKeyChange = true; | ||
}); | ||
extensions.push(added); | ||
} | ||
} | ||
function changePropsOrRemoveOrChangeElements() { | ||
const relevantProperties = [ | ||
{ name: 'doc' }, | ||
{ name: '@sql.prepend' }, | ||
{ name: '@sql.append' }, | ||
]; | ||
const changedProperties = {}; | ||
@@ -102,5 +124,5 @@ | ||
relevantProperties.forEach(prop => { | ||
if (artifact[prop.name] !== otherArtifact[prop.name] && (!prop.beta || isBetaEnabled(options, prop.beta))) { | ||
changedProperties[prop.name] = changedElement(artifact[prop.name], otherArtifact[prop.name] || null); | ||
Object.keys(relevantProperties).forEach(prop => { | ||
if (artifact[prop] !== otherArtifact[prop]) { | ||
changedProperties[prop] = changedElement(artifact[prop], otherArtifact[prop] || null); | ||
} | ||
@@ -163,2 +185,7 @@ }); | ||
migration.remove = removedElements; | ||
if(!hasPrimaryKeyChange) | ||
forEach(removedElements, (_name, change) => { | ||
if(change.key && !change.target) | ||
hasPrimaryKeyChange = true; | ||
}); | ||
} | ||
@@ -169,2 +196,11 @@ | ||
migration.change = changedElements; | ||
if(!hasPrimaryKeyChange) | ||
forEach(changedElements, (_name, change) => { | ||
if((change.old.key || change.new.key) && !change.new.target && !change.old.target) { | ||
// For to.hdi.migration: Just drop-create (commented out), for to.sql.migration: Handle case where we add/remove "key" keyword, no drop-create otherwise | ||
if(options.sqlDialect === 'hana' && options.src === 'hdi' || (!change.old.key || !change.new.key)) { | ||
hasPrimaryKeyChange = true; | ||
} | ||
} | ||
}); | ||
} | ||
@@ -202,2 +238,5 @@ | ||
changePropsOrRemoveOrChangeElements(); | ||
if(hasPrimaryKeyChange) { | ||
changedPrimaryKeys.push(name); | ||
} | ||
}; | ||
@@ -327,2 +366,9 @@ } | ||
const relevantProperties = { | ||
'doc': true, | ||
'@sql.prepend': true, | ||
'@sql.append': true | ||
}; | ||
/** | ||
@@ -335,3 +381,19 @@ * Returns whether any type parameters differ between two given elements. Ignores whether types themselves differ (`type` property). | ||
function typeParametersChanged(element, otherElement) { | ||
return !deepEqual(element, otherElement, (key, depth) => !(depth === 0 && key === 'type')); | ||
const checked = new Set(); | ||
for (const key in element) { | ||
if (Object.prototype.hasOwnProperty.call(element, key)) | ||
if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type') { | ||
checked.add(key); | ||
if(!deepEqual(element[key], otherElement[key])) | ||
return true; | ||
} | ||
} | ||
for (const key in otherElement) { | ||
if (Object.prototype.hasOwnProperty.call(otherElement, key)) | ||
if((!key.startsWith('@') || relevantProperties[key]) && key !== 'type' && !checked.has(key)) | ||
return true; | ||
} | ||
return false; | ||
} | ||
@@ -338,0 +400,0 @@ |
@@ -41,2 +41,5 @@ 'use strict'; | ||
delete constraintRemovals[name]; | ||
}, | ||
function primaryKey() { | ||
return false; | ||
} | ||
@@ -50,3 +53,3 @@ ), | ||
function getFilterObject( dialect, extensionCallback, migrationCallback, removeConstraintsCallback ) { | ||
function getFilterObject( dialect, extensionCallback, migrationCallback, removeConstraintsCallback, primaryKeyCallback ) { | ||
return { | ||
@@ -71,10 +74,8 @@ // will be called with a simple Array.filter, as we need to filter constraint `ADD` for SQLite | ||
// will be called with a Array.forEach | ||
migration: ({ | ||
change, migrate, remove, removeConstraints, | ||
}, { error, warning, message }) => { | ||
forEach(remove, (name) => { | ||
error('def-unsupported-element-drop', [ 'definitions', migrate, 'elements', name ], {}, 'Dropping elements is not supported'); | ||
migration: (migrations, { error, warning, message }) => { | ||
forEach(migrations.remove, (name) => { | ||
error('def-unsupported-element-drop', [ 'definitions', migrations.migrate, 'elements', name ], {}, 'Dropping elements is not supported'); | ||
}); | ||
forEach(change, (name, migration) => { | ||
const loc = [ 'definitions', migrate, 'elements', name ]; | ||
forEach(migrations.change, (name, migration) => { | ||
const loc = [ 'definitions', migrations.migrate, 'elements', name ]; | ||
if (migration.new.type === migration.old.type && migration.new.length < migration.old.length) | ||
@@ -89,5 +90,5 @@ error('type-unsupported-length-change', loc, { id: name }, 'Changed element $(ID) is a length reduction and is not supported'); | ||
else if (dialect !== 'sqlite' && isKey(migration.new) && !isKey(migration.old)) // key added/changed - pg, hana and sqlite do not support it, h2 probably also - issues when data is in the table already | ||
message('type-unsupported-key-change', [ 'definitions', migrate, 'elements', name ], { id: name, '#': 'changed' } ); | ||
message('type-unsupported-key-change', [ 'definitions', migrations.migrate, 'elements', name ], { id: name, '#': 'changed' } ); | ||
else if (migrationCallback) | ||
migrationCallback(migrate, name, migration, change, error); | ||
migrationCallback(migrations.migrate, name, migration, migrations.change, error); | ||
@@ -100,4 +101,4 @@ // TODO: precision/scale growth | ||
constraintTypes.forEach((constraintType) => { | ||
forEach(removeConstraints?.[constraintType], (name, constraint) => { | ||
removeConstraintsCallback(removeConstraints[constraintType], name, constraint, warning); | ||
forEach(migrations.removeConstraints?.[constraintType], (name, constraint) => { | ||
removeConstraintsCallback(migrations.removeConstraints[constraintType], name, constraint, warning); | ||
}); | ||
@@ -111,2 +112,8 @@ }); | ||
}, | ||
changedPrimaryKeys: (changedPrimaryKeyArtifactName) => { | ||
if (primaryKeyCallback) | ||
return primaryKeyCallback(changedPrimaryKeyArtifactName); | ||
return true; | ||
}, | ||
}; | ||
@@ -113,0 +120,0 @@ } |
@@ -230,2 +230,3 @@ 'use strict'; | ||
.option(' --odata-vocabularies <list>') | ||
.option(' --odata-open-type') | ||
.option('-c, --csn') | ||
@@ -263,2 +264,3 @@ .option('-f, --odata-format <format>', ['flat', 'structured']) | ||
{ prefix: { alias, ns, uri }, ... } | ||
--odata-open-type Renders all structured types as OpenType=true, if not annotated otherwise. | ||
-n, --sql-mapping <style> Annotate artifacts and elements with "@cds.persistence.name", which is | ||
@@ -265,0 +267,0 @@ the corresponding database name (see "--sql-mapping" for "toHana or "toSql") |
@@ -5,2 +5,3 @@ { | ||
"rules": { | ||
"cds-compiler/message-no-quotes": "off", | ||
"prefer-const": "error", | ||
@@ -15,3 +16,2 @@ "quotes": ["error", "single", "avoid-escape"], | ||
"class-methods-use-this": "off", | ||
// Who cares - just very whiny and in the way | ||
"complexity": "off", | ||
@@ -18,0 +18,0 @@ "max-len": "off", |
@@ -70,2 +70,3 @@ | ||
optionProcessor.verifyOptions(options, 'toSql', true) | ||
// eslint-disable-next-line cds-compiler/message-template-string | ||
.forEach(complaint => warning(null, null, `${complaint}`)); | ||
@@ -72,0 +73,0 @@ |
@@ -37,3 +37,5 @@ | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toRename').forEach(complaint => warning(null, null, `${complaint}`)); | ||
optionProcessor.verifyOptions(options, 'toRename') | ||
// eslint-disable-next-line cds-compiler/message-template-string | ||
.forEach(complaint => warning(null, null, `${complaint}`)); | ||
checkCSNVersion(inputCsn, options); | ||
@@ -40,0 +42,0 @@ |
@@ -337,2 +337,77 @@ // Common render functions for toCdl.js, toHdbcds.js and toSql.js | ||
/** | ||
* Maps $-variables per SQL dialect to a renderable expression. | ||
* Callers can use `.fallback` in case the wanted dialect is not found. | ||
* | ||
* IMPORTANT: There is no sqlDialect better-sqlite. This "fake" dialect is | ||
* set in variableForDialect() below. | ||
* | ||
* @type {object} | ||
*/ | ||
const variablesToSql = { | ||
fallback: { | ||
// no fallback for $user.id and $user.tenant -> warning in call-site | ||
'$user.locale': '\'en\'', | ||
// $at.* are handled in all dialects -> there is no need for a fallback | ||
}, | ||
hana: { | ||
'$user.id': "SESSION_CONTEXT('APPLICATIONUSER')", | ||
'$user.locale': "SESSION_CONTEXT('LOCALE')", | ||
'$user.tenant': "SESSION_CONTEXT('TENANT')", | ||
'$at.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))", | ||
'$at.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))", | ||
}, | ||
postgres: { | ||
'$user.id': "current_setting('cap.applicationuser')", | ||
'$user.locale': "current_setting('cap.locale')", | ||
'$user.tenant': "current_setting('cap.tenant')", | ||
'$at.from': "current_setting('cap.valid_from')::timestamp", | ||
'$at.to': "current_setting('cap.valid_to')::timestamp", | ||
}, | ||
'better-sqlite': { | ||
'$user.id': "session_context( '$user.id' )", | ||
'$user.locale': "session_context( '$user.locale' )", | ||
'$user.tenant': "session_context( '$user.tenant' )", | ||
'$at.from': "session_context( '$valid.from' )", | ||
'$at.to': "session_context( '$valid.to' )", | ||
}, | ||
sqlite: { | ||
// For sqlite, we render the string-format-time (strftime) function. | ||
// Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas | ||
// the format for timestamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z' | ||
// --> Therefore the comparison in the temporal where clause doesn't work properly. | ||
'$at.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')", | ||
// + 1ms compared to $at.from | ||
'$at.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')", | ||
}, | ||
plain: { | ||
'$at.from': 'current_timestamp', | ||
'$at.to': 'current_timestamp', | ||
}, | ||
h2: { | ||
'$user.id': '@applicationuser', | ||
'$user.locale': '@locale', | ||
'$user.tenant': '@tenant', | ||
'$at.from': '@valid_from', | ||
'$at.to': '@valid_to', | ||
}, | ||
}; | ||
/** | ||
* Get a renderable string for given variable for the given options.sqlDialect. | ||
* Note that this function does not handle `variableReplacements`. Callers should | ||
* first check if the user has specified them and use them instead. | ||
* | ||
* @param {SqlOptions} options Used for `sqlDialect` and better-sqlite option. | ||
* @param {string} variable Variable to render, e.g. `$user.id`. | ||
* @return {string|null} `null` if the variable could not be found for the given dialect and in the fallback values. | ||
*/ | ||
function variableForDialect( options, variable ) { | ||
const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables | ||
? 'better-sqlite' | ||
: options.sqlDialect; | ||
return variablesToSql[dialect]?.[variable] || variablesToSql.fallback[variable] || null; | ||
} | ||
/** | ||
* Get the element matching the column | ||
@@ -618,2 +693,3 @@ * | ||
cdsToHdbcdsTypes, | ||
variableForDialect, | ||
hasHanaComment, | ||
@@ -620,0 +696,0 @@ getHanaComment, |
@@ -74,3 +74,3 @@ 'use strict'; | ||
*/ | ||
alterColumns(artifactName, columnName, delta, definitionsStr) { | ||
alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) { | ||
return [ `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER (${definitionsStr});` ]; | ||
@@ -181,3 +181,3 @@ } | ||
*/ | ||
alterColumns(artifactName, columnName, delta, definitionsStr) { | ||
alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) { | ||
const sqls = []; | ||
@@ -189,3 +189,17 @@ if (delta.new.notNull === true || delta.new.key === true) | ||
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};`); | ||
if (delta.old.default && !delta.old.value) // Drop old default if any exists | ||
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`); | ||
if (delta.new.default && !delta.new.value ) { // Alter column with default | ||
const df = delta.new.default; | ||
delete delta.new.default; | ||
const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env); | ||
delta.new.default = df; | ||
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`); | ||
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`); | ||
} | ||
else { // Alter column without default | ||
sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};`); | ||
} | ||
if (delta.new.notNull && !delta.old.notNull) | ||
@@ -192,0 +206,0 @@ sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${columnName} SET NOT NULL;`); |
@@ -21,3 +21,3 @@ // API functions returning the SQL identifier token text for a name | ||
// this situation: it constructs delimited identifiers for the reserved names. | ||
// Other names are returned directly to to avoid that people think that they | ||
// Other names are returned directly to avoid that people think that they | ||
// had to use all-upper names in CDS. | ||
@@ -24,0 +24,0 @@ |
@@ -6,2 +6,3 @@ { | ||
"rules": { | ||
"cds-compiler/message-no-quotes": "off", | ||
"prefer-const": "error", | ||
@@ -8,0 +9,0 @@ "quotes": ["error", "single", "avoid-escape"], |
@@ -16,2 +16,3 @@ 'use strict'; | ||
const { setProp } = require('../../base/model'); | ||
const { xprInAnnoProperties } = require('../../compiler/builtins'); | ||
@@ -44,2 +45,3 @@ | ||
$origin: () => {}, // no-op | ||
'@': annotation, | ||
}; | ||
@@ -53,3 +55,3 @@ | ||
for (const name of Object.getOwnPropertyNames( parent )) | ||
standard( parent, name, parent[name] ); | ||
dictEntry( parent, name, parent[name] ); | ||
} | ||
@@ -73,3 +75,2 @@ else { | ||
!{}.propertyIsEnumerable.call( _parent, _prop ) || | ||
(typeof _prop === 'string' && _prop.startsWith('@')) || | ||
(options.skipIgnore && node.$ignore) || | ||
@@ -88,3 +89,3 @@ options.skipStandard?.[_prop] | ||
for (const name of Object.getOwnPropertyNames( node )) { | ||
const trans = transformers[name] || standard; | ||
const trans = transformers[name] || transformers[name.charAt(0)] || standard; | ||
if (customTransformers[name]) | ||
@@ -113,3 +114,3 @@ customTransformers[name](node, name, node[name], csnPath, _parent, _prop); | ||
for (const name of Object.getOwnPropertyNames( node )) { | ||
const trans = transformers[name] || standard; | ||
const trans = transformers[name] || transformers[name.charAt(0)] || standard; | ||
if (customTransformers[name]) | ||
@@ -143,2 +144,26 @@ customTransformers[name](node, name, node[name], csnPath, dict); | ||
/** | ||
* Transformer for things that are annotations. When we have a "=" plus an expression of some sorts, | ||
* we treat it like a "standard" thing. | ||
* | ||
* @param {object | Array} _parent the thing that has _prop | ||
* @param {string|number} _prop the name of the current property or index | ||
* @param {object} node The value of node[_prop] | ||
*/ | ||
function annotation( _parent, _prop, node ) { | ||
if (options.processAnnotations) { | ||
if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) { | ||
standard(_parent, _prop, node); | ||
} | ||
else if (node && typeof node === 'object') { | ||
csnPath.push(_prop); | ||
for (const name of Object.getOwnPropertyNames( node )) | ||
annotation( node, name, node[name] ); | ||
csnPath.pop(); | ||
} | ||
} | ||
} | ||
/** | ||
* Special version of "dictionary" to apply artifactTransformers. | ||
@@ -289,2 +314,3 @@ * | ||
* @property {boolean} [directDict=false] Implicitly set via applyTransformationsOnDictionary | ||
* @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them | ||
*/ |
@@ -16,5 +16,7 @@ 'use strict'; | ||
* @param {string} pathDelimiter | ||
* @param {object} [iterateOptions={}] | ||
* @param {CSN.Options} [options={}] | ||
* @returns {CSN.Model} Return the input csn, with the transformations applied | ||
*/ | ||
function attachOnConditions( csn, csnUtils, pathDelimiter ) { | ||
function attachOnConditions( csn, csnUtils, pathDelimiter, iterateOptions = {}, options = {} ) { | ||
const { isManagedAssociation } = csnUtils; | ||
@@ -31,4 +33,4 @@ | ||
} | ||
}, /* only for views and entities */ | ||
}, [], { skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') }); | ||
}, /* only for views and entities */ | ||
}, [], Object.assign({ skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') }, iterateOptions)); | ||
@@ -52,3 +54,3 @@ return csn; | ||
let joinWithAnd = false; | ||
if (elem.keys.length === 0) { // TODO: really kill instead of $ignore? | ||
if (elem.keys.length === 0 && options.transformation !== 'effective') { // TODO: really kill instead of $ignore? | ||
elem.$ignore = true; | ||
@@ -61,4 +63,6 @@ } | ||
ref: [ | ||
...elemName.startsWith('$') ? [ '$self' ] : [], | ||
elemName, | ||
].concat(foreignKey.ref), | ||
...foreignKey.ref, | ||
], | ||
}; | ||
@@ -68,2 +72,3 @@ const fkName = `${elemName}${pathDelimiter}${foreignKey.as || implicitAs(foreignKey.ref)}`; | ||
ref: [ | ||
...fkName.startsWith('$') ? [ '$self' ] : [], | ||
fkName, | ||
@@ -112,5 +117,6 @@ ], | ||
* @param {string} pathDelimiter | ||
* @param {boolean} [processOnInQueries=false] Wether to process on-conditions in queries (joins and mixins) | ||
* @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition | ||
*/ | ||
function getFKAccessFinalizer( csn, csnUtils, pathDelimiter ) { | ||
function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries = false ) { | ||
const { | ||
@@ -183,6 +189,12 @@ inspectRef, | ||
if (artifact.query || artifact.projection) { | ||
applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', { | ||
orderBy: (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path), | ||
groupBy: (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path), | ||
}, {}, [ 'definitions', artifactName ]); | ||
const transform = (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path); | ||
const queryTransformers = { | ||
orderBy: transform, | ||
groupBy: transform, | ||
where: transform, | ||
having: transform, | ||
}; | ||
if (processOnInQueries) | ||
queryTransformers.on = transform; | ||
applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, {}, [ 'definitions', artifactName ]); | ||
} | ||
@@ -189,0 +201,0 @@ |
@@ -31,3 +31,3 @@ 'use strict'; | ||
// mixin elements must be transformed, why can't toSql also use mixins? | ||
if (artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type')) | ||
if (options.transformation === 'effective' && artifact.elements || artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type')) | ||
processDict(artifact.elements, path.concat([ 'elements' ])); | ||
@@ -70,2 +70,5 @@ if (artifact.query?.SELECT?.mixin) | ||
elem.on = processExpressionArgs(elem.on, pathToOn); | ||
const column = csnUtils.getColumn(elem); | ||
if (column?.cast?.on) // avoid difference between column and element | ||
column.cast.on = elem.on; | ||
@@ -192,3 +195,4 @@ /** | ||
elem.$ignore = true; | ||
if (options.transformation !== 'effective') | ||
elem.$ignore = true; | ||
return []; | ||
@@ -195,0 +199,0 @@ } |
@@ -36,3 +36,3 @@ 'use strict'; | ||
const artifact = csn.definitions[path[1]]; | ||
csnUtils.initDefinition(artifact); // potentially no initialized, yet | ||
csnUtils.initDefinition(artifact); // potentially not initialized, yet | ||
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) { | ||
@@ -723,3 +723,3 @@ const root = csnUtils.get$combined({ SELECT: parent }); | ||
else { // the thing is not shadowed - use the name from the base | ||
const col = { ref: [ part ] }; | ||
const col = part.startsWith('$') ? { ref: [ base[part][0].parent, part ] } : { ref: [ part ] }; | ||
if (isComplexQuery) // $env: tableAlias | ||
@@ -726,0 +726,0 @@ setProp(col, '$env', base[part][0].parent); |
@@ -276,10 +276,2 @@ 'use strict'; | ||
const lastAssoc = getLastAssoc(current, exprPath.concat(i)); | ||
// toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by | ||
// '“EXISTS” can only be used with associations/compositions, found $(TYPE)' | ||
// But the error might not be clear, since it could be because of our rewritten stuff. The later check | ||
// checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error | ||
if (lastAssoc.tail.length > 0) | ||
error(null, current.$path, { id: lastAssoc.tail[0].id ? lastAssoc.tail[0].id : lastAssoc.tail[0], name: lastAssoc.ref.id ? lastAssoc.ref.id : lastAssoc.ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"'); | ||
const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ]; | ||
@@ -316,7 +308,2 @@ expr[i].ref = newThing; | ||
if (!root.target) { | ||
error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)'); | ||
return { result: [], leftovers: [] }; | ||
} | ||
const subselect = getSubselect(root.target, ref, sources); | ||
@@ -336,3 +323,3 @@ | ||
if (ref && ref.where) { | ||
const remappedWhere = remapExistingWhere(target, ref.where); | ||
const remappedWhere = remapExistingWhere(target, ref.where, exprPath, current); | ||
if (remappedWhere.length > 3) | ||
@@ -387,3 +374,3 @@ subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]); | ||
if (current.$scope === '$self') { | ||
error('ref-unexpected-exists-self', current.$path, { id: current.ref[0], name: 'exists' }, 'With $(NAME), path steps must not start with $(ID)'); | ||
error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' }); | ||
return []; | ||
@@ -580,23 +567,2 @@ } | ||
/** | ||
* Get the last association from the expression part - similar to getFirstAssoc | ||
* | ||
* @param {object} xprPart | ||
* @param {CSN.Path} path | ||
* @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The last assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail). | ||
*/ | ||
function getLastAssoc( xprPart, path ) { | ||
const { links, art } = inspectRef(path); | ||
for (let i = xprPart.ref.length - 1; i > -1; i--) { | ||
if (links[i].art && links[i].art.target) { | ||
return { | ||
head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1), | ||
}; | ||
} | ||
} | ||
return { | ||
head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [], | ||
}; | ||
} | ||
/** | ||
* Check (using inspectRef -> links), whether the first path step is an entity or query source | ||
@@ -721,9 +687,17 @@ * | ||
* | ||
* This function also rejects $self paths in filter conditions. | ||
* | ||
* @param {string} target | ||
* @param {TokenStream} where | ||
* @returns {TokenStream} The input-where with the refs transformed to absolute ones | ||
* @param {CSN.Path} path path to the part, used if error needs to be thrown | ||
* @param {CSN.Artifact} parent the host of the `where`, used if error needs to be thrown | ||
* | ||
* @returns {TokenStream} where The input-where with the refs transformed to absolute ones | ||
*/ | ||
function remapExistingWhere( target, where ) { | ||
function remapExistingWhere( target, where, path, parent ) { | ||
return where.map((part) => { | ||
if (part.ref && part.$scope !== '$magic') { | ||
if (part.$scope === '$self') { | ||
error('ref-unexpected-self', path, { '#': 'exists-filter', elemref: parent, id: part.ref[0] }); | ||
} | ||
else if (part.ref && part.$scope !== '$magic') { | ||
part.ref = [ target, ...part.ref ]; | ||
@@ -730,0 +704,0 @@ return part; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
hasAnnotationValue, getServiceNames, forEachDefinition, | ||
getResultingName, forEachMemberRecursively, | ||
getResultingName, forEachMemberRecursively, applyAnnotationsFromExtensions, | ||
} = require('../../model/csnUtils'); | ||
@@ -33,5 +33,8 @@ const { setProp, isDeprecatedEnabled } = require('../../base/model'); | ||
const { error, warning } = messageFunctions; | ||
const generatedArtifacts = Object.create(null); | ||
forEachDefinition(csn, generateDraft); | ||
applyAnnotationsFromExtensions(csn, { filter: name => generatedArtifacts[name], applyToElements: false }); | ||
/** | ||
@@ -122,2 +125,4 @@ * Generate the draft stuff for a given artifact | ||
generatedArtifacts[draftsArtifactName] = true; | ||
// extract keys for UUID inspection | ||
@@ -138,3 +143,3 @@ const keys = []; | ||
else { | ||
const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0); | ||
const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.UUID' || k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0); | ||
if (uuidCount === 0) | ||
@@ -150,2 +155,3 @@ warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”'); | ||
if (!draftAdminDataProjection) { | ||
generatedArtifacts[draftAdminDataProjectionName] = true; | ||
draftAdminDataProjection = createAndAddDraftAdminDataProjection(matchingService, true); | ||
@@ -155,2 +161,7 @@ | ||
draftAdminDataProjection.projection.columns = Object.keys(draftAdminDataProjection.elements).map(e => (e === 'DraftUUID' ? { key: true, ref: [ 'DraftAdministrativeData', e ] } : { ref: [ 'DraftAdministrativeData', e ] })); | ||
if (options.transformation === 'effective' && draftAdminDataProjection.projection) { | ||
draftAdminDataProjection.query = { SELECT: draftAdminDataProjection.projection }; | ||
delete draftAdminDataProjection.projection; | ||
} | ||
} | ||
@@ -168,3 +179,3 @@ | ||
// Duplicate the artifact as a draft shadow entity | ||
if (csn.definitions[persistenceName]) { | ||
if (csn.definitions[persistenceName] && !(options.transformation === 'effective' && csn.definitions[persistenceName].kind === 'entity' && csn.definitions[persistenceName].elements.DraftAdministrativeData_DraftUUID)) { | ||
const definingDraftRoot = draftRoots.get(csn.definitions[persistenceName]); | ||
@@ -171,0 +182,0 @@ if (!definingDraftRoot) { |
'use strict'; | ||
const { forEachDefinition, getServiceNames } = require('../../model/csnUtils'); | ||
const { forEachDefinition, getServiceNames, applyAnnotationsFromExtensions } = require('../../model/csnUtils'); | ||
const { forEach } = require('../../utils/objectUtils'); | ||
const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils'); | ||
const { getTransformers } = require('../transformUtils'); | ||
const { ModelError } = require('../../base/error'); | ||
const { makeMessageFunction } = require('../../base/messages'); | ||
@@ -32,3 +31,2 @@ | ||
const { | ||
createForeignKeyElement, | ||
createAndAddDraftAdminDataProjection, createScalarElement, | ||
@@ -56,3 +54,3 @@ createAssociationElement, createAssociationPathComparison, | ||
const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name)); | ||
const filterDict = Object.create(null); | ||
forEachDefinition(csn, (def, defName) => { | ||
@@ -66,2 +64,3 @@ // Generate artificial draft fields for entities/views if requested, ignore if not part of a service | ||
applyAnnotationsFromExtensions(csn, { override: true, filter: name => filterDict[name] }); | ||
return csn; | ||
@@ -83,7 +82,2 @@ /** | ||
function generateDraftForOdata( artifact, artifactName, rootArtifact ) { | ||
// Sanity check | ||
// @ts-ignore | ||
if (!isArtifactInSomeService(artifactName, services)) | ||
throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`); | ||
// Nothing to do if already draft-enabled (composition traversal may have circles) | ||
@@ -113,5 +107,7 @@ if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) && | ||
resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]); | ||
filterDict[artifactName] = true; | ||
} | ||
else { | ||
resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]); | ||
filterDict[artifactName] = true; | ||
} | ||
@@ -149,11 +145,2 @@ | ||
// Note that we need to do the ODATA transformation steps for managed associations | ||
// (foreign key field generation, generatedFieldName) by hand, because the corresponding | ||
// transformation steps have already been done on all artifacts when we come here) | ||
let uuidDraftKey = draftAdministrativeData.DraftAdministrativeData.keys.filter(key => key.ref && key.ref.length === 1 && key.ref[0] === 'DraftUUID'); | ||
if (uuidDraftKey && uuidDraftKey[0]) { | ||
uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element | ||
const path = [ 'definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ]; | ||
createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path); | ||
} | ||
// SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...) | ||
@@ -160,0 +147,0 @@ const siblingEntity = createAssociationElement('SiblingEntity', artifactName, false); |
@@ -6,3 +6,3 @@ 'use strict'; | ||
const { | ||
applyTransformations, forEachDefinition, forEachMemberRecursively, implicitAs, cloneCsnNonDict, | ||
applyTransformations, forEachDefinition, forEachMemberRecursively, implicitAs, cloneCsnNonDict, forEachMember, applyTransformationsOnNonDictionary, | ||
} = require('../../model/csnUtils'); | ||
@@ -37,3 +37,3 @@ const associations = require('../db/associations'); | ||
// Flatten out the fks and create the corresponding elements | ||
flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, warning, '_', true, csnUtils, { allowArtifact: a => a.kind === 'entity' }); | ||
flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, error, warning, '_', true, csnUtils, { allowArtifact: () => true, skipDict: {} }); | ||
@@ -44,21 +44,52 @@ // Add the foreign keys also to the columns if the association itself was explicitly selected | ||
applyTransformations(csn, { | ||
columns: (parent, prop, columns) => { | ||
const newColumns = []; | ||
for (const col of columns) { | ||
newColumns.push(col); | ||
const element = csnUtils.getElement(col); | ||
if (element && element.keys) | ||
element.keys.forEach(fk => addForeignKeyToColumns(fk, newColumns, col, options)); | ||
} | ||
parent.columns = newColumns; | ||
columns: expandManagedToFksInArray(), | ||
groupBy: expandManagedToFksInArray(true), | ||
orderBy: expandManagedToFksInArray(true), | ||
}, [], { allowArtifact: artifact => artifact.query !== undefined || artifact.projection !== undefined }); | ||
forEachDefinition(csn, associations.getFKAccessFinalizer(csn, csnUtils, '_', true)); | ||
applyTransformations(csn, { | ||
elements: (_parent, prop, elements, path) => { | ||
forEachMember(_parent, (element, elementName, _prop, elPath) => { | ||
if (element.on) { | ||
applyTransformationsOnNonDictionary(element, 'on', { | ||
ref: (parent, __prop, ref) => { | ||
if (ref[0] === '$self' && ref.length > 1 && !ref[1].startsWith('$')) // TODO: Is this safe? | ||
parent.ref = ref.slice(1); | ||
}, | ||
}, {}, elPath); | ||
} | ||
}, path); | ||
}, | ||
}, [], { allowArtifact: artifact => artifact.kind === 'entity' }); | ||
forEachDefinition(csn, associations.getFKAccessFinalizer(csn, csnUtils, '_')); | ||
}); | ||
// Calculate the on-conditions from the .keys - | ||
associations.attachOnConditions(csn, csnUtils, '_'); | ||
associations.attachOnConditions(csn, csnUtils, '_', { allowArtifact: () => true }, options); | ||
return csn; | ||
/** | ||
* Expand managed associations in an array and insert them in-place | ||
* | ||
* If requested, leave out the assocs themselves | ||
* | ||
* @param {boolean} [killAssoc=false] | ||
* @returns {Function} applyTransformationsCallback | ||
*/ | ||
function expandManagedToFksInArray( killAssoc = false ) { | ||
return function expand(parent, prop, array, path) { | ||
const newColumns = []; | ||
for (let i = 0; i < array.length; i++) { | ||
const col = array[i]; | ||
const element = csnUtils.getElement(col) || col.ref && csnUtils.inspectRef(path.concat(prop, i)).art; | ||
if (!killAssoc || !element?.keys) | ||
newColumns.push(col); | ||
if (element?.keys) | ||
element.keys.forEach(fk => addForeignKeyToColumns(fk, newColumns, col, options)); | ||
} | ||
parent[prop] = newColumns; | ||
}; | ||
} | ||
} | ||
/** | ||
@@ -65,0 +96,0 @@ * FKs need to be added to the .columns |
@@ -37,2 +37,4 @@ 'use strict'; | ||
delete csn.vocabularies; // must not be set for effective CSN | ||
const { expandStructsInExpression } = transformUtils.getTransformers(csn, options, '_'); | ||
@@ -62,7 +64,7 @@ queries.projectionToSELECTAndAddColumns(csn); | ||
const resolveTypesInActionsAfterFlattening = types.resolve(csn, csnUtils); | ||
// Expand a structured thing in: keys, columns, order by, group by | ||
expansion.expandStructureReferences(csn, options, '_', messageFunctions, csnUtils); | ||
const resolveTypesInActionsAfterFlattening = types.resolve(csn, csnUtils); | ||
csnUtils = getUtils(csn, 'init-all'); | ||
@@ -78,2 +80,5 @@ | ||
// ensure getElement works on flattened struct_assoc columns | ||
csnUtils = getUtils(csn, 'init-all'); | ||
processCalculatedElementsInEntities(csn); | ||
@@ -80,0 +85,0 @@ associations.managedToUnmanaged(csn, options, csnUtils, messageFunctions); |
'use strict'; | ||
const { | ||
forEachDefinition, forEachMemberRecursively, getArtifactDatabaseNameOf, getElementDatabaseNameOf, forEachMember, | ||
forEachDefinition, forEachMemberRecursively, getArtifactDatabaseNameOf, getElementDatabaseNameOf, applyTransformations, | ||
} = require('../../model/csnUtils'); | ||
@@ -17,12 +17,17 @@ /** | ||
forEachDefinition(csn, (artifact, artifactName) => { | ||
if (artifact.kind === 'entity') { | ||
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact); | ||
addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifactName, options.sqlMapping, csn, options.sqlDialect), artifact); | ||
forEachMemberRecursively(artifact, (member, memberName) => addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.sqlMapping, options.sqlDialect), member), [ 'definitions', artifactName ]); | ||
} | ||
forEachMemberRecursively(artifact, (member, memberName) => addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.sqlMapping, options.sqlDialect), member), [ 'definitions', artifactName ]); | ||
}); | ||
} | ||
const artifactPropertiesToRemove = [ 'includes' ]; | ||
const memberPropertiesToRemove = [ 'localized', 'enum', 'keys' ]; | ||
/** | ||
* Delete the given prop from parent. | ||
* | ||
* @param {object} parent | ||
* @param {string|number} prop | ||
*/ | ||
function killProp( parent, prop ) { | ||
delete parent[prop]; | ||
} | ||
@@ -40,24 +45,38 @@ /** | ||
*/ | ||
function removeDefinitionsAndProperties( csn ) { | ||
forEachDefinition(csn, (artifact, artifactName) => { | ||
if (artifact.kind === 'aspect' || artifact.kind === 'type') { | ||
delete csn.definitions[artifactName]; | ||
} | ||
else { | ||
if (artifact['@cds.persistence.skip'] === 'if-unused') | ||
function _removeDefinitionsAndProperties( csn ) { | ||
const killers = { | ||
$ignore: (a, b, c, path, parentParent) => { | ||
const tail = path[path.length - 1]; | ||
delete parentParent[tail]; | ||
}, | ||
kind: (artifact, a, b, path) => { | ||
if (artifact.kind === 'aspect' || artifact.kind === 'type') | ||
delete csn.definitions[path[1]]; | ||
else if (artifact['@cds.persistence.skip'] === 'if-unused') | ||
artifact['@cds.persistence.skip'] = false; | ||
for (const prop of artifactPropertiesToRemove) | ||
delete artifact[prop]; | ||
}, | ||
// Still used in flattenStructuredElements - in db/flattening.js | ||
_flatElementNameWithDots: killProp, | ||
// Set when setting default string/binary length - used in copyTypeProperties and fixBorkedElementsOfLocalized | ||
// to not copy the .length property if it was only set via default | ||
$default: killProp, | ||
// Set when we turn UUID into String, checked during generateDraftForHana | ||
$renamed: killProp, | ||
// Set when we remove .key from temporal things, used in localized.js | ||
$key: killProp, | ||
includes: killProp, | ||
localized: killProp, | ||
enum: killProp, | ||
keys: killProp, | ||
excluding: killProp, // * is resolved, so has no effect anymore | ||
}; | ||
forEachMember(artifact, (member) => { | ||
for (const prop of memberPropertiesToRemove) | ||
delete member[prop]; | ||
}); | ||
} | ||
}); | ||
applyTransformations(csn, killers, [], { skipIgnore: false }); | ||
} | ||
module.exports = { | ||
attachPersistenceName, | ||
removeDefinitionsAndProperties, | ||
removeDefinitionsAndProperties: _removeDefinitionsAndProperties, | ||
}; |
@@ -14,25 +14,23 @@ 'use strict'; | ||
forEachDefinition(csn, (artifact) => { | ||
if (artifact.kind === 'entity') { | ||
if (artifact.projection) { | ||
if (!artifact.projection.columns) | ||
artifact.projection.columns = [ '*' ]; | ||
artifact.query = { SELECT: artifact.projection }; | ||
delete artifact.projection; | ||
redoProjections.push(() => { | ||
if (artifact.query) { | ||
artifact.projection = artifact.query.SELECT; | ||
delete artifact.query; | ||
if (artifact.$syntax === 'projection') | ||
delete artifact.$syntax; | ||
} | ||
}); | ||
} | ||
else if (artifact.query) { | ||
applyTransformationsOnNonDictionary(artifact, 'query', { | ||
SELECT: (parent, prop, SELECT) => { | ||
SELECT.columns ??= [ '*' ]; | ||
}, | ||
}); | ||
} | ||
if (artifact.projection) { | ||
if (!artifact.projection.columns) | ||
artifact.projection.columns = [ '*' ]; | ||
artifact.query = { SELECT: artifact.projection }; | ||
delete artifact.projection; | ||
redoProjections.push(() => { | ||
if (artifact.query) { | ||
artifact.projection = artifact.query.SELECT; | ||
delete artifact.query; | ||
if (artifact.$syntax === 'projection') | ||
delete artifact.$syntax; | ||
} | ||
}); | ||
} | ||
else if (artifact.query) { | ||
applyTransformationsOnNonDictionary(artifact, 'query', { | ||
SELECT: (parent, prop, SELECT) => { | ||
SELECT.columns ??= [ '*' ]; | ||
}, | ||
}); | ||
} | ||
}); | ||
@@ -39,0 +37,0 @@ |
@@ -6,2 +6,3 @@ 'use strict'; | ||
} = require('../../model/csnUtils'); | ||
const { forEachKey } = require('../../utils/objectUtils'); | ||
@@ -37,3 +38,3 @@ /** | ||
later.push(artifact.actions); | ||
} ], { skipDict: { actions: true } }); | ||
} ], { skipDict: { actions: true }, processAnnotations: true }); | ||
@@ -69,3 +70,6 @@ return function resolveTypesInActions() { | ||
else if (final?.type) { | ||
parent.type = final.type; | ||
forEachKey(final, (key) => { // copy `type` + properties (default, etc.) | ||
if (parent[key] === undefined || key === 'type') | ||
parent[key] = final[key]; | ||
}); | ||
} | ||
@@ -72,0 +76,0 @@ |
@@ -164,2 +164,6 @@ 'use strict'; | ||
// - Generate artificial draft fields on a structured CSN if requested, flattening and struct | ||
// expansion do their magic including foreign key generation and annotation propagation. | ||
generateDrafts(csn, options, services); | ||
// Check if structured elements and managed associations are compared in an expression | ||
@@ -171,2 +175,3 @@ // and expand these structured elements. This tuple expansion allows all other | ||
if (!structuredOData) { | ||
@@ -216,3 +221,2 @@ expansion.expandStructureReferences(csn, options, '_', { error, info, throwWithAnyError }, csnUtils, { skipArtifact: isExternalServiceMember }); | ||
// Now all artificially generated things are in place | ||
// - Generate artificial draft fields if requested | ||
// TODO: should be done by the compiler - Check associations for valid foreign keys | ||
@@ -225,3 +229,2 @@ // TODO: check if needed at all: Remove '$projection' from paths in the element's ON-condition | ||
// - Perform checks for exposed non-abstract entities and views - check media type and key-ness | ||
generateDrafts(csn, options, services) | ||
@@ -228,0 +231,0 @@ // Deal with all kind of annotations manipulations here |
@@ -651,3 +651,3 @@ 'use strict'; | ||
if (!ignoreUnknownExtensions) { | ||
messageFunctions.message('anno-undefined-art', [ 'extensions', index ], | ||
messageFunctions.message('ext-undefined-def', [ 'extensions', index ], | ||
{ art: name }); | ||
@@ -654,0 +654,0 @@ } |
@@ -37,3 +37,3 @@ // @ts-nocheck | ||
* @param {Object} state Object | ||
* anno: Don't eliminate arrays with single entry in statetations (TODO?) as they are collections | ||
* anno: Don't eliminate arrays with single entry in expressions (TODO?) as they are collections | ||
* array: Bias AST representation. | ||
@@ -43,3 +43,4 @@ * nary: return n-ary or binary tree | ||
function parseExpr(xpr, state = { anno: 0, array: true, nary: false }) { | ||
function parseExpr(xpr, state = { array: true, nary: false }) { | ||
state.anno = 0; | ||
// Notes: | ||
@@ -52,5 +53,26 @@ // - Variables `s` and `e` are used as index variables into `xpr`s for start and end. | ||
function parseExprInt(xpr, state) { | ||
return conditionOR(...CaseWhen(xpr), state); | ||
return conditionOR(...CaseWhen(Cast(xpr, state)), state); | ||
} | ||
function Cast(xpr, state) { | ||
if(xpr != null && !state.array) { | ||
if(Array.isArray(xpr)) | ||
return xpr.map(x => Cast(x, state)); | ||
if(typeof xpr === 'object') { | ||
const castKeys = Object.keys(xpr).filter(k => k !== 'cast'); | ||
if(xpr.cast != null && castKeys.length === 1) { | ||
return { 'cast': [ xpr.cast, { [castKeys[0]]: xpr[castKeys[0]] } ] }; | ||
} | ||
else { | ||
for(let n in xpr) { | ||
// xpr could be an array with polluted prototype | ||
if (Object.hasOwnProperty.call(xpr, n)) | ||
xpr[n] = Cast(xpr[n], state) | ||
} | ||
} | ||
} | ||
} | ||
return xpr; | ||
} | ||
function CaseWhen(xpr) { | ||
@@ -101,7 +123,7 @@ if(Array.isArray(xpr)) { | ||
if(xpr[whenPos] === 'when' && whenPos - (casePos+1) >= 1) { | ||
const arg = xpr.slice(casePos+1, whenPos) | ||
const caseExpr = xpr.slice(casePos+1, whenPos) | ||
if(state.array) | ||
caseTree.push(arg); | ||
caseTree.push(caseExpr); | ||
else | ||
caseTree.case.push(arg); | ||
caseTree.case.push(caseExpr.length === 1 ? caseExpr[0] : caseExpr); | ||
} | ||
@@ -119,7 +141,7 @@ | ||
if(xpr[thenPos] === 'then') { | ||
const when = xpr.slice(whenPos+1, thenPos); | ||
const whenExpr = xpr.slice(whenPos+1, thenPos); | ||
if(state.array) | ||
caseTree.push(when); | ||
caseTree.push(whenExpr); | ||
else | ||
when.when.push(when.length === 1 ? when[0] : when); | ||
when.when.push(whenExpr.length === 1 ? whenExpr[0] : whenExpr); | ||
} | ||
@@ -196,2 +218,4 @@ | ||
let i = s; | ||
let not = false; | ||
let between; | ||
while(i < e && xpr[i] !== 'between') i++; | ||
@@ -203,7 +227,7 @@ const b = i < e ? i : -1; | ||
let token = [ 'between' ]; | ||
const not = (xpr[b-1] === 'not'); | ||
not = (xpr[b-1] === 'not'); | ||
if(not) | ||
token.splice(0,0, 'not'); | ||
const expr = expression(xpr, s, not ? b-1 : b, state); | ||
const between = state.array | ||
between = state.array | ||
? [ expr, ...token ] | ||
@@ -227,2 +251,5 @@ : { 'between': [ expr ] }; | ||
} | ||
if(not && !state.array) { | ||
between = { 'not': between } | ||
} | ||
return between; | ||
@@ -274,3 +301,6 @@ } | ||
if(xpr[s] === '+' || xpr[s] === '-' || (!state.array && xpr[s] === 'new')) { | ||
return [ xpr[s], unary(xpr, s+1, e, state) ]; | ||
if(state.array) | ||
return [ xpr[s], unary(xpr, s+1, e, state) ]; | ||
else | ||
return { [xpr[s]]: unary(xpr, s+1, e, state) }; | ||
} | ||
@@ -289,3 +319,3 @@ } | ||
if(Array.isArray(xpr) && xpr.length > 0) { | ||
if(e-s <= 1 && state.anno === 0) | ||
if(e-s <= 1 && state.anno === 0 && typeof xpr[e-1] !== 'string') | ||
return parseExprInt(xpr[e-1], state); | ||
@@ -324,3 +354,4 @@ else | ||
function binaryExpr(xpr, token, next, s, e, state) { | ||
const expr = []; | ||
const naryExpr = []; | ||
let not = false; | ||
if (Array.isArray(xpr)) { | ||
@@ -330,3 +361,3 @@ let [tl, p] = findToken(s, e); | ||
let lhs = next(xpr, s, p, state); | ||
expr.push(lhs); | ||
naryExpr.push(lhs); | ||
let op = xpr.slice(p, p+tl); | ||
@@ -337,4 +368,13 @@ s = p+tl; | ||
let rhs = next(xpr, s, p, state); | ||
expr.push(...op, rhs); | ||
lhs = state.array ? [ lhs, ...op, rhs ] : { [op.join('')]: [lhs, rhs] }; | ||
naryExpr.push(...op, rhs); | ||
if(state.array) | ||
lhs = [ lhs, ...op, rhs ]; | ||
else { | ||
not = op.length > 1 && op[0] === 'not'; | ||
if(not) | ||
op = op.slice(1); | ||
lhs = (not | ||
? { 'not': { [op.join('')]: [lhs, rhs] } } | ||
: { [op.join('')]: [lhs, rhs] }); | ||
} | ||
op = xpr.slice(p, p+tl); | ||
@@ -344,7 +384,19 @@ s = p+tl; | ||
} | ||
expr.push(...op, next(xpr, s, e, state)); | ||
let rhs = next(xpr, s, e, state); | ||
if(Array.isArray(rhs) && rhs.length === 0) | ||
rhs = undefined; | ||
naryExpr.push(...op, rhs); | ||
if (state.array) | ||
return (state.nary ? expr : [ lhs, ...op, next(xpr, s, e, state) ]) | ||
else | ||
return { [op.join('')]: [lhs, next(xpr, s, e, state)] }; | ||
return (state.nary ? naryExpr : [ lhs, ...op, rhs ]) | ||
else { | ||
not = op.length > 1 && op[0] === 'not'; | ||
if(not) | ||
op = op.slice(1); | ||
return (not | ||
? { 'not': { [op.join('')]: [ lhs, rhs ] } } | ||
: { [op.join('')]: [ lhs, rhs ] }); | ||
} | ||
} | ||
@@ -351,0 +403,0 @@ } |
@@ -17,4 +17,4 @@ // | ||
const stderrHasColor = process.stderr.isTTY; | ||
const stdoutHasColor = process.stdout.isTTY; | ||
const stderrHasColor = process.stderr?.isTTY; | ||
const stdoutHasColor = process.stdout?.isTTY; | ||
@@ -21,0 +21,0 @@ // Note: We require both stderr and stdout to be TTYs, as we don't |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "4.3.2", | ||
"version": "4.4.0", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -35,2 +35,3 @@ "homepage": "https://cap.cloud.sap/", | ||
"generateCompilerRefs": "cross-env MAKEREFS='true' mocha test/testCompiler.js", | ||
"generateMigrationRefs": "cross-env MAKEREFS='true' mocha test/test.to.migration.js", | ||
"generateEdmRefs": "cross-env MAKEREFS='true' mocha test/testEdmPositive.js", | ||
@@ -37,0 +38,0 @@ "generateForHanaRefs": "cross-env MAKEREFS='true' mocha test/testHanaTransformation.js", |
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
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 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
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
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
4664444
203
95160