@sap/cds-compiler
Advanced tools
Comparing version 1.24.4 to 1.26.2
@@ -87,2 +87,18 @@ #!/usr/bin/env node | ||
// Internally, parseCdl is an option so we map the command to it. | ||
if (cmdLine.command === 'parseCdl') { | ||
cmdLine.command = 'toCsn'; | ||
cmdLine.options.parseCdl = true; | ||
if (cmdLine.args.files.length > 1) { | ||
const err = `'parseCdl' expects exactly one file! ${cmdLine.args.files.length} provided.`; | ||
displayUsage(err, optionProcessor.commands['parseCdl'].helpText, 2); | ||
} | ||
} | ||
if (cmdLine.options.beta) { | ||
const features = cmdLine.options.beta.split(','); | ||
cmdLine.options.beta = {}; | ||
features.forEach((val) => cmdLine.options.beta[val] = true); | ||
} | ||
// Do the work for the selected command (default 'toCsn') | ||
@@ -89,0 +105,0 @@ executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args); |
@@ -9,2 +9,90 @@ # ChangeLog for cdx compiler and backends | ||
## Version 1.26.2 - 2020-04-24 | ||
### Added | ||
- The client tool `cdsc` has got a new option `--beta <list>` which may be used to | ||
specify a comma separated list of experimental features to be enabled. | ||
- CSN in parse-cdl mode now has a `requires` property that represents `using`s from CDL. | ||
### Fixed | ||
- OData: | ||
+ Change foreign key creation order for associations to respect their dependencies. | ||
+ Use correct path during on-condition flattening. | ||
+ Report error when using elements without types for **array of type of (element)** and similar definitions. | ||
- HANA/SQL: | ||
+ Fix references to `null` enum values in default clauses. | ||
- Type arguments are now properly set in CSN when using parse-cdl mode. | ||
- Avoid unjust warning if the `extensions` property of an input CSN contain `extend` statements. | ||
## Version 1.26.0 - 2020-04-17 | ||
### Added | ||
- The client tool `cdsc` has got a new command `parseCdl` which returns a CSN | ||
that is close to the original CDL file. It does not resolve imports and does | ||
not apply extensions. | ||
- Unmanaged associations as primary keys are now warned about. | ||
- `localized` in combination with `key` is now warned about. | ||
- Enum values are now checked to only be either numbers or a strings - a warning is raised. | ||
- Elements in mixin clauses that are _not_ unmanaged associations now produce an error. | ||
### Changed | ||
- HANA/SQL: | ||
+ Raise warnings `rewrite-not-supported` and `rewrite-undefined-key` to errors. | ||
- Compiler: Empty elements are now kept along for the propagation. | ||
- OData: Annotate all elements of `DraftAdministrativeData` with `@Common.Label: '{i18n>"Draft_<elementName>"}'` | ||
and elements 'DraftUUID', 'DraftIsCreatedByMe' and 'DraftIsProcessedByMe' with `@UI.Hidden`. | ||
### Fixed | ||
- Compiler: `type of <unmanaged assocation>` is now handled correctly by raising an error. | ||
## Version 1.25.0 - 2020-04-09 | ||
### Changed | ||
- Downgrade `chained array of`-error to a warning | ||
- SQLite: Don't render implicit casts | ||
## Version 1.24.6 - 2020-04-08 | ||
### Changed | ||
- OData: | ||
+ Improve messages for misaligned forward/backlink associations in EDM generator | ||
+ For V2 add annotations `@sap.creatable: false`, `@sap.updatable: false`, `@sap.deletable: false`, | ||
`@sap.pageable: false` to the Parameter EntityType and `@sap.creatable: false`, `@sap.updatable: false`, | ||
`@sap.deletable: false`, `@sap.addressable: false` to the Result EntityType. | ||
+ Update vocabularies 'Common' and 'Graph' and 'ODM'. | ||
### Fixed | ||
- Various messages mention more appropriate source locations. | ||
- Improve messages for `array of` | ||
- OData: | ||
+ Render 'array of' for ReturnType correctly | ||
+ Report error for view fields with no type information early | ||
+ Handle associations in structures with an association as explicit key | ||
### Removed | ||
- The client tool `cdsc` does not offer the option `--std-json-parser` anymore, | ||
as it had no effect. | ||
## Version 1.24.4 - 2020-03-25 | ||
@@ -11,0 +99,0 @@ |
@@ -9,2 +9,3 @@ /** @module API */ | ||
const alerts = require('../base/alerts'); | ||
const { emptyLocation } = require('../base/location'); | ||
const { | ||
@@ -398,2 +399,5 @@ sortMessages, messageString, CompilationError, getMessageFunction, handleMessages, | ||
function recompile(csn, options) { | ||
// Explicitly set parseCdl to false because backends cannot handle | ||
// the option and is only intended for CDL sources. | ||
options.parseCdl = false; | ||
/* eslint-disable global-require */ | ||
@@ -435,7 +439,9 @@ const { augment } = require('../json/from-csn'); | ||
throw err; | ||
const message = getMessageFunction( csn, options ); | ||
message( 'api-recompiled-csn', emptyLocation('csn.json'), | ||
null, {}, 'Info', 'CSN input had to be recompiled' ); | ||
// next line to be replaced by CSN parser call which reads the CSN object | ||
const xsn = getXsn(csn, options); | ||
const message = getMessageFunction( xsn, options ); | ||
message( 'api-recompiled-csn', { filename: 'csn.json', start: { offset: 0, line: 1, column: 1 } }, | ||
null, {}, 'Info', 'CSN input had to be recompiled' ); | ||
return processor( compactModel(xsn), options, ...args ); | ||
@@ -442,0 +448,0 @@ } |
@@ -33,2 +33,3 @@ 'use strict'; | ||
'testMode', | ||
'compatibility', | ||
]; | ||
@@ -35,0 +36,0 @@ |
@@ -41,3 +41,3 @@ // Implementation of alerts | ||
signal.warning = warning; | ||
signal.ino = info; | ||
signal.info = info; | ||
@@ -44,0 +44,0 @@ let config = options.severities || {}; |
@@ -85,34 +85,2 @@ // Functions for dictionaries (Objects without prototype) | ||
// Return the source location of the complete dictionary `dict`. If | ||
// `extraLocation` is truthy, also consider this location. | ||
// ASSUMPTION: all entries in the dictionary have a property `location` and | ||
// `location.filename` has always the same value. | ||
function dictLocation( dict, extraLocation ) { | ||
if (!dict) | ||
return extraLocation; | ||
if (!(dict instanceof Array)) | ||
dict = Object.getOwnPropertyNames( dict ).map( name => dict[name] ); | ||
let locations = [].concat( ...dict.map( objLocations ) ); | ||
if (extraLocation) | ||
locations.push( extraLocation ); | ||
let min = locations.reduce( (a,b) => a.start.offset < b.start.offset ? a : b ); | ||
let max = locations.reduce( (a,b) => (a.end||a.start).offset > (b.end||b.start).offset ? a : b ); | ||
return { filename: min.filename, start: min.start, end: max.end }; | ||
} | ||
function objLocations( obj ) { | ||
return (obj instanceof Array) ? obj.map( o => o.location ) : [ obj.location ]; | ||
} | ||
// Create a location with location properties `filename` and `start` from | ||
// argument `start`, and location property `end` from argument `end`. | ||
function combinedLocation( start, end ) { | ||
return { | ||
filename: start.location.filename, | ||
start: start.location.start, | ||
end: end.location.end | ||
}; | ||
} | ||
// Push `entry` to the array value with key `name` in the dictionary `dict`. | ||
@@ -137,4 +105,2 @@ function pushToDict( dict, name, entry ) { | ||
clearDict, | ||
dictLocation, | ||
combinedLocation, | ||
pushToDict, | ||
@@ -141,0 +107,0 @@ forEachInDict, |
@@ -6,3 +6,3 @@ const { CompileMessage } = require('../base/messages'); | ||
* @param {any} msg | ||
* @param {XSN.Location|CSN.Location|any[]} location | ||
* @param {XSN.Location|CSN.Location|CSN.Path} location | ||
* @param {string} severity | ||
@@ -18,6 +18,6 @@ * @param {string} message_id | ||
location = searchForLocation(semanticLocation); | ||
} | ||
return new CompileMessage(location, msg, severity ? severity : msg._severity, message_id, beautifySemanticLocation(semanticLocation), context); | ||
/** @param {CSN.Path} semanticLocation */ | ||
function searchForLocation(semanticLocation){ | ||
@@ -41,2 +41,3 @@ let last_location = null; // Don't display a location if we cannot find one! | ||
/** @param {CSN.Path} semanticLocation */ | ||
function validateSemanticLocation(semanticLocation){ | ||
@@ -59,2 +60,3 @@ | ||
/** @param {CSN.Path} semanticLocation */ | ||
function beautifySemanticLocation(semanticLocation){ | ||
@@ -61,0 +63,0 @@ if(!semanticLocation){ |
@@ -6,2 +6,3 @@ // Functions and classes for syntax messages | ||
const term = require('../utils/term'); | ||
const { normalizeLocation } = require('./location'); | ||
@@ -65,2 +66,6 @@ // For messageIds, where no severity has been provided via code (central def) | ||
/** | ||
* Returns true if at least one of the given messages is of severity "Error" | ||
* @param {CSN.Message[]} messages | ||
*/ | ||
function hasErrors( messages ) { | ||
@@ -70,11 +75,16 @@ return messages && messages.some( m => m.severity === 'Error' ); | ||
// Return gnu-style error string for location `loc`: | ||
// - 'File:Line:Col' without `loc.end` | ||
// - 'File:Line:StartCol-EndCol' if Line = start.line = end.line | ||
// - 'File:StartLine.StartCol-EndLine.EndCol' otherwise | ||
function locationString( loc, normalizeFilename ) { | ||
if (!loc) | ||
/** | ||
* Return gnu-style error string for location `loc`: | ||
* - 'File:Line:Col' without `loc.end` | ||
* - 'File:Line:StartCol-EndCol' if Line = start.line = end.line | ||
* - 'File:StartLine.StartCol-EndLine.EndCol' otherwise | ||
* | ||
* @param {CSN.Location|XSN.Location} location | ||
* @param {boolean} [normalizeFilename] | ||
*/ | ||
function locationString( location, normalizeFilename ) { | ||
if (!location) | ||
return '<???>'; | ||
loc = normalizeLocation( loc ); | ||
let filename = (loc.filename && (normalizeFilename || normalizeFilename === 0)) | ||
const loc = normalizeLocation( location ); | ||
let filename = (loc.filename && normalizeFilename) | ||
? loc.filename.replace( /\\/g, '/' ) | ||
@@ -145,3 +155,3 @@ : loc.filename; | ||
* @param {any} location Location of the message | ||
* @param {any} msg The message text | ||
* @param {string} msg The message text | ||
* @param {string} [severity='Error'] Severity: Debug, Info, Warning, Error | ||
@@ -171,16 +181,2 @@ * @param {string} [id] The ID of the message - visible as property messageId | ||
// Normalize location: to old-style at the moment, TODO: should switch to new-style | ||
function normalizeLocation( loc ) { | ||
if (!loc || !loc.file ) | ||
return loc; | ||
let location = { | ||
filename: loc.file, | ||
start: { line: loc.line, column: loc.col || 0 }, | ||
end: { line: loc.endLine || loc.line, column: loc.endCol || loc.col || 0 }, | ||
}; | ||
if (!loc.endLine) | ||
location.$weak = true; | ||
return location; | ||
} | ||
/** | ||
@@ -349,2 +345,3 @@ * Handle compiler messages, i.e. throw a compiler exception if there are | ||
/** @param {XSN.Location} loc */ | ||
function weakLocation( loc ) { | ||
@@ -361,3 +358,3 @@ // use return { ...location, $weak: true } if that is JS standard | ||
* | ||
* @param {object} err | ||
* @param {CSN.Message} err | ||
* @param {boolean} [normalizeFilename] | ||
@@ -374,3 +371,2 @@ * @param {boolean} [noMessageId] | ||
(!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')'); | ||
} | ||
@@ -385,3 +381,3 @@ | ||
* | ||
* @param {object} err | ||
* @param {CSN.Message} err | ||
* @param {boolean} [normalizeFilename] | ||
@@ -410,3 +406,3 @@ * @param {boolean} [noMessageId] | ||
* @param {string[]} sourceLines The source code split up into lines, e.g. by `src.split('\n')` | ||
* @param {object} err Error object containing all details like line, message, etc. | ||
* @param {CSN.Message} err Error object containing all details like line, message, etc. | ||
*/ | ||
@@ -462,6 +458,10 @@ function messageContext(sourceLines, err) { | ||
} | ||
// Compare two messages `a` and `b`. Return 0 if they are equal, 1 if `a` is | ||
// larger than `b`, and -1 if `a` is smaller than `b`. Messages without a location | ||
// are considered larger than messages with a location. | ||
/** | ||
* Compare two messages `a` and `b`. Return 0 if they are equal, 1 if `a` is | ||
* larger than `b`, and -1 if `a` is smaller than `b`. Messages without a location | ||
* are considered larger than messages with a location. | ||
* | ||
* @param {CSN.Message} a | ||
* @param {CSN.Message} b | ||
*/ | ||
function compareMessage( a, b ) { | ||
@@ -569,5 +569,4 @@ if (a.location && b.location) { | ||
CompilationError, | ||
normalizeLocation, | ||
translatePathLocations | ||
} | ||
@@ -43,4 +43,4 @@ // | ||
// Apply function `callback(member, memberName)` to each member in `construct`, | ||
// recursively (i.e. also for sub-elements of elements). | ||
// Apply function `callback(member, memberName, prop)` to each member in | ||
// `construct`, recursively (i.e. also for sub-elements of elements). | ||
function forEachMemberRecursively( construct, callback ) { | ||
@@ -58,2 +58,45 @@ forEachMember( construct, ( member, memberName, prop ) => { | ||
/** | ||
* Apply function `callback` to all members of object `obj` (main artifact or | ||
* parent member). Members are considered those in dictionaries `elements`, | ||
* `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also | ||
* searched inside property `items` (array of). `$queries`, `mixin` and | ||
* `columns` are also visited in contrast to `forEachMember()`. | ||
* See function `forEachGeneric()` for details. | ||
* | ||
* @param {XSN.Artifact} construct | ||
* @param {(member: object, memberName: string, prop: string) => any} callback | ||
* @param {object} [target] | ||
*/ | ||
function forEachMemberWithQuery( construct, callback, target ) { | ||
let obj = construct.returns || construct; // why the extra `returns` for actions? | ||
obj = obj.items || obj; | ||
forEachGeneric( target || obj, 'elements', callback ); | ||
forEachGeneric( obj, 'enum', callback ); | ||
forEachGeneric( obj, 'foreignKeys', callback ); | ||
forEachGeneric( construct, 'actions', callback ); | ||
forEachGeneric( construct, 'params', callback ); | ||
// For Queries | ||
forEachGeneric( construct, '$queries', callback ); | ||
forEachGeneric( construct, 'mixin', callback ); | ||
forEachGeneric( construct, 'columns', callback ); | ||
} | ||
/** | ||
* Apply function `callback(member, memberName, prop)` to each member in | ||
* `construct`, recursively (i.e. also for sub-elements of elements). | ||
* In contrast to `forEachMemberRecursively()` this function also traverses | ||
* queries and mixins. | ||
* | ||
* @param {XSN.Artifact} construct | ||
* @param {(member: object, memberName: string, prop: string) => any} callback | ||
*/ | ||
function forEachMemberRecursivelyWithQuery( construct, callback ) { | ||
forEachMemberWithQuery( construct, ( member, memberName, prop ) => { | ||
callback( member, memberName, prop ); | ||
// Descend into nested members, too | ||
forEachMemberRecursivelyWithQuery( member, callback ); | ||
}); | ||
} | ||
// Apply function `callback` to all objects in dictionary `dict`, including all | ||
@@ -167,2 +210,4 @@ // duplicates (found under the same name). Function `callback` is called with | ||
forEachMemberRecursively, | ||
forEachMemberWithQuery, | ||
forEachMemberRecursivelyWithQuery, | ||
forEachGeneric, | ||
@@ -169,0 +214,0 @@ forEachInOrder, |
'use strict'; | ||
const alerts = require('../base/alerts'); | ||
const { getMessageFunction } = require('../base/messages'); | ||
const { hasArtifactTypeInformation } = require('../model/modelUtils') | ||
@@ -12,3 +14,7 @@ // Semantic checks that are performed on artifacts | ||
if (!art.abstract && emptyOrOnlyVirtualElements(art)) { | ||
signal(info`Dubious entity or type without non-virtual elements`, art.location, undefined, 'empty-entity-or-type'); | ||
const location = art.name && art.name.location || art.location; | ||
if (art.kind === 'entity') | ||
signal(info`Dubious entity without non-virtual elements`, location, undefined, 'empty-entity'); | ||
else if (art.kind === 'type') | ||
signal(info`Dubious type without non-virtual elements`, location, undefined, 'empty-type'); | ||
} | ||
@@ -50,5 +56,90 @@ | ||
/** | ||
* If the given artifact is a type definition then check whether it is | ||
* properly defined and has valid type information, e.g. information about | ||
* its elements or references another valid type. | ||
* | ||
* @param {XSN.Definition} artifact | ||
* @param {XSN.Model} model | ||
*/ | ||
function checkTypeDefinitionHasType(artifact, model) { | ||
if (artifact.kind !== 'type') | ||
return; | ||
checkArtifactHasProperType(artifact, model); | ||
} | ||
/** | ||
* Check that the given artifact has proper type information. | ||
* Either the artifact itself is a proper type or its `type` property | ||
* references a proper type (including `many type of`). | ||
* | ||
* @param {XSN.Definition} artifact | ||
* @param {XSN.Model} model | ||
*/ | ||
function checkArtifactHasProperType(artifact, model) { | ||
function warnAboutMissingType(art) { | ||
// Can happen in CSN, e.g. `{ a: { kind: "type" } }` but should | ||
// not happen in CDL. | ||
const message = getMessageFunction(model); | ||
message('check-proper-type', art.location, art, { art: artifact }, | ||
['Error'], { | ||
std: 'Dubious type $(ART) without type information', | ||
element: 'Dubious element $(MEMBER) of $(ART) without type information', | ||
}); | ||
} | ||
if (!hasArtifactTypeInformation(artifact)) { | ||
warnAboutMissingType(artifact); | ||
return; | ||
} | ||
// Check for `type of` | ||
if (artifact.type) { | ||
checkTypeOfHasProperType(artifact, model) | ||
return; | ||
} | ||
const items = artifact.items; | ||
if (!items) | ||
return; | ||
// `array of` without nested `type of` | ||
if (!hasArtifactTypeInformation(items)) | ||
warnAboutMissingType(items); | ||
else if (items && items.type) | ||
// `array of type of` | ||
checkTypeOfHasProperType(items, model) | ||
} | ||
/** | ||
* Check that the `type of` information in the given artifact (i.e. `type` property) | ||
* has proper type information or warn otherwise. The artifact's final type is checked. | ||
* | ||
* @param {XSN.Artifact} artifact | ||
* @param {XSN.Model} model | ||
*/ | ||
function checkTypeOfHasProperType(artifact, model) { | ||
if (!artifact.type) | ||
return; | ||
const finalType = artifact.type._artifact && artifact.type._artifact._finalType; | ||
if (finalType && !hasArtifactTypeInformation(finalType)) { | ||
const message = getMessageFunction(model); | ||
message('check-proper-type-of', artifact.type.location, artifact, { art: artifact.type }, | ||
'Info', { | ||
std: 'Referred type of $(ART) does not contain proper type information', | ||
element: 'Referred element $(MEMBER) of $(ART) does not contain proper type information', | ||
}); | ||
return; | ||
} | ||
} | ||
module.exports = { | ||
checkNotEmptyOrOnlyVirtualElems, | ||
checkNoUnmanagedAssocsInGroupByOrderBy, | ||
checkTypeDefinitionHasType, | ||
checkTypeOfHasProperType, | ||
checkArtifactHasProperType, | ||
}; |
@@ -5,7 +5,10 @@ 'use strict'; | ||
const alerts = require('../base/alerts'); | ||
const { checkArtifactHasProperType } = require('./checkArtifacts'); | ||
const { isComposition } = require('../model/modelUtils.js') | ||
function checkPrimaryKeyTypeCompatibility(elem, model) { | ||
const { error, signal } = alerts(model); | ||
/** | ||
* Run primary key checks on the given element, for example type checks. | ||
*/ | ||
function checkPrimaryKey(elem, model) { | ||
const { error, warning, signal } = alerts(model); | ||
let type = ''; | ||
@@ -17,4 +20,23 @@ // apparently this is the resolved type (over an derived type chain) | ||
/** | ||
* Check that a primary key element is not an unmanaged association or contains unmanaged associations | ||
* | ||
* @param {any} element Element to check recursively | ||
*/ | ||
function checkForUnmanagedAssociations(element){ | ||
if(element.on){ | ||
signal(warning`Unmanaged associations cannot be used as primary key`, elem.key.location); | ||
} | ||
// Recursively check sub-elements for structured types | ||
if(element.elements){ | ||
for(let elemName of Object.keys(element.elements)){ | ||
const subElement = element.elements[elemName]; | ||
checkForUnmanagedAssociations(subElement); | ||
} | ||
} | ||
} | ||
/** | ||
* Check that a primary key element is not array-like or contains array-like elements | ||
* | ||
* | ||
* @param {any} element Element to check recursively | ||
@@ -35,3 +57,12 @@ */ | ||
} | ||
function checkLocalizedSubElement(element) { | ||
if (!element.localized || element.localized.val !== true) | ||
return; | ||
if (element._parent && element._parent.kind === 'element') { | ||
signal('Keyword "localized" is ignored for sub elements', element.localized.location, 'Warning', 'localized-sub-element'); | ||
} | ||
} | ||
if(elem.key && elem.key.val === true){ | ||
@@ -42,4 +73,6 @@ if(['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(type)){ | ||
checkForUnmanagedAssociations(elem); | ||
checkKeyArrayLike(elem); | ||
} | ||
checkLocalizedSubElement(elem); | ||
} | ||
@@ -382,4 +415,42 @@ | ||
/** | ||
* If the given element is not computed, check whether its final type has | ||
* proper type information. Useful for checking that `type of` does not refer | ||
* to a computed element. | ||
* | ||
* @param {XSN.Artifact} element | ||
* @param {XSN.Model} model | ||
*/ | ||
function checkElementHasValidTypeOf(element, model) { | ||
// Computed elements, e.g. "1+1 as foo" in a view don't have a valid type and | ||
// are skipped here. Their usage will still be warned about, though. | ||
// Elements in projections are not tested as well as they don't have the | ||
// origin's type information copied but reference them in _finalType. | ||
if (element['@Core.Computed'] || element.origin) | ||
return; | ||
checkArtifactHasProperType(element, model); | ||
} | ||
/** | ||
* Check that there are no .items containing .items. | ||
* | ||
* Type definitions and elements are checked - recursion is handled by the semanticChecks | ||
* | ||
* @param {object} obj element or type definition | ||
* @param {object} model the whole model | ||
*/ | ||
function checkForItemsChain(obj, model){ | ||
if(obj.items) { | ||
const itemsType = obj.items.type ? obj.items.type._artifact : obj.items; | ||
if(itemsType.items){ | ||
const { signal, warning } = alerts(model); | ||
signal(warning`"Array of"/"many" must not be chained - ${obj.name.id}, ${itemsType.name.id}.`, obj.location, 'Warning', 'chained-array-of'); | ||
} | ||
} | ||
} | ||
module.exports = { | ||
checkPrimaryKeyTypeCompatibility, | ||
checkPrimaryKey, | ||
checkManagedAssoc, | ||
@@ -390,3 +461,5 @@ checkVirtualElement, | ||
checkLocalizedElement, | ||
checkStructureCasting | ||
checkStructureCasting, | ||
checkElementHasValidTypeOf, | ||
checkForItemsChain | ||
}; |
@@ -7,9 +7,9 @@ 'use strict'; | ||
* Associations inside of an array-like must have all their foreign keys inside of the items. | ||
* | ||
* This effectively restricts it to | ||
* - managed associations or | ||
* | ||
* This effectively restricts it to | ||
* - managed associations or | ||
* - unmanaged associations where the on-condition only references elements inside | ||
of the items, $self usage must be forbidden. | ||
* | ||
* @param {object} member Member | ||
* | ||
* @param {CSN.Artifact} member Member | ||
*/ | ||
@@ -16,0 +16,0 @@ function validateAssociationsInItems(member){ |
@@ -10,5 +10,6 @@ 'use strict'; | ||
const keywords = require('../base/keywords'); | ||
const { checkNotEmptyOrOnlyVirtualElems, checkNoUnmanagedAssocsInGroupByOrderBy } = require('./checkArtifacts'); | ||
const { checkPrimaryKeyTypeCompatibility, checkVirtualElement, checkManagedAssoc, checkCardinality, | ||
checkLocalizedElement, checkStructureCasting } = require('./checkElements'); | ||
const { checkNotEmptyOrOnlyVirtualElems, checkNoUnmanagedAssocsInGroupByOrderBy, | ||
checkTypeDefinitionHasType } = require('./checkArtifacts'); | ||
const { checkPrimaryKey, checkVirtualElement, checkManagedAssoc, checkCardinality, | ||
checkLocalizedElement, checkStructureCasting, checkForItemsChain, checkElementHasValidTypeOf } = require('./checkElements'); | ||
const { checkExpression } = require('./checkExpressions'); | ||
@@ -79,3 +80,3 @@ const { foreachPath } = require('../model/modelUtils'); | ||
entity: checkEntity, | ||
enum: nothingToCheckYet, | ||
enum: checkEnum, | ||
event: nothingToCheckYet, | ||
@@ -117,3 +118,3 @@ function: checkActionOrFunction, | ||
* Called for each main artifact (no need to iterate its members). | ||
* @param {object} artifact A "model.definitions" artifact | ||
* @param {CSN.Artifact} artifact | ||
*/ | ||
@@ -211,2 +212,29 @@ function checkGenericArtifact(artifact) { | ||
/** | ||
* Enum specific checks. For example this function checks that the value type | ||
* of enum values are allowed. | ||
* | ||
* @param {XSN.Artifact} enumNode | ||
*/ | ||
function checkEnum(enumNode) { | ||
if (!enumNode.value) | ||
return; | ||
const type = enumNode.value.literal; | ||
const loc = enumNode.value.location; | ||
// Special handling to print a more detailed error message | ||
if (type === 'enum') { | ||
message('enum-value-ref', loc, enumNode, { }, 'Warning', | ||
'References to other values are not allowed as enum values'); | ||
return; | ||
} | ||
const allowedValueTypes = ['number', 'string']; | ||
if (!allowedValueTypes.includes(type)) { | ||
message('enum-value-type', loc, enumNode, { }, 'Warning', | ||
'Only strings or numbers are allowed as enum values'); | ||
} | ||
} | ||
// Traverses 'node' recursively and applies 'checkExpression' to all expressions | ||
@@ -292,9 +320,13 @@ // found within paths (e.g. filters, parameters, ...) | ||
checkManagedAssoc(type, model); | ||
checkTypeDefinitionHasType(type, model); | ||
checkForItemsChain(type, model); | ||
} | ||
function checkElement(elem) { | ||
checkPrimaryKeyTypeCompatibility(elem, model); | ||
checkElementHasValidTypeOf(elem, model); | ||
checkPrimaryKey(elem, model); | ||
checkVirtualElement(elem, model); | ||
checkManagedAssoc(elem, model); | ||
checkCardinality(elem, model); | ||
checkForItemsChain(elem, model); | ||
if (elem.onCond && !elem.onCond.$inferred) { | ||
@@ -301,0 +333,0 @@ checkExpression(elem.onCond, model); |
@@ -151,9 +151,36 @@ // Consistency checker on model (XSN = augmented CSN) | ||
_assocSources: { kind: true, test: TODO }, // just null: isArray( inDefinitions ) during resolve | ||
$magicVariables: { test: TODO }, | ||
$magicVariables: { | ||
// $magicVariables contains "builtin" artifacts that differ from | ||
// "normal artifacts" and therefore have a custom schema | ||
requires: [ 'kind', 'artifacts' ], | ||
schema: { | ||
kind: { test: isString, enum: [ '$magicVariables' ] }, | ||
artifacts: { | ||
// Do not use "normal" definitions spec because of these artifacts | ||
// are missing the location property | ||
test: isDictionary( definition ), | ||
requires: [ 'kind', 'name' ], | ||
optional: [ 'elements', '$autoElement', '_finalType', '_deps' ], | ||
schema: { | ||
kind: { test: isString, enum: [ 'builtin' ] }, | ||
name: { test: isObject, requires: [ 'id', 'element' ] }, | ||
$autoElement: { test: isString }, | ||
// missing location for normal "elements" | ||
elements: { test: TODO }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
$builtins: { test: TODO }, | ||
builtin: { kind: true, test: builtin }, | ||
$internal: { test: TODO }, | ||
$internal: { | ||
test: standard, | ||
requires: [ '$frontend' ], | ||
schema: { | ||
$frontend: { test: isString, enum: [ '$internal' ] }, | ||
}, | ||
}, | ||
version: { test: TODO }, // TODO: describe - better: 'header' | ||
$version: { test: isString }, | ||
meta: { test: TODO }, | ||
meta: { test: TODO }, // never tested due to --test-mode | ||
namespace: { | ||
@@ -438,3 +465,23 @@ test: (model.$frontend !== 'json') ? standard : TODO, | ||
queries: { kind: true, test: TODO }, // TODO: $queries with other structure | ||
$queries: { kind: [ 'entity', 'view' ], test: TODO }, | ||
$queries: { | ||
kind: [ 'entity', 'view' ], | ||
test: isArray(), | ||
requires: [ | ||
'kind', 'location', 'name', | ||
'_parent', '_main', '_$next', '_block', | ||
// query specific | ||
'op', 'from', 'elements', | ||
'$combined', | ||
'$tableAliases', '_firstAliasInFrom', | ||
'queries', // TODO: deprecated? | ||
], | ||
optional: [ | ||
'_finalType', | ||
'_deps', | ||
// query specific | ||
'where', 'columns', 'mixin', 'quantifier', 'offset', | ||
'orderBy', '$orderBy', 'groupBy', 'exclude', 'having', | ||
'limit', '_tableAlias', '_elementsIndexNo', | ||
], | ||
}, | ||
_leadingQuery: { kind: true, test: TODO }, | ||
@@ -459,3 +506,3 @@ $navigation: { kind: true, test: TODO }, | ||
_entities: { test: TODO }, | ||
$compositionTargets: { test: TODO }, | ||
$compositionTargets: { test: isDictionary( isBoolean ) }, | ||
$lateExtensions: { test: TODO }, | ||
@@ -462,0 +509,0 @@ _ancestors: { kind: [ 'type', 'entity', 'view' ], test: isArray( TODO ) }, |
@@ -6,2 +6,3 @@ // The builtin artifacts of CDS | ||
const { forEachInDict } = require('../base/dictionaries'); | ||
const { builtinLocation } = require('../base/location'); | ||
@@ -105,16 +106,9 @@ const core = { | ||
kind: 'namespace', | ||
name: { absolute: name, location: createDummyLocation() }, | ||
// buitlin namespaces don't have a cds file, so no location available | ||
name: { absolute: name, location: builtinLocation() }, | ||
blocks: [], | ||
artifacts: Object.create(null), | ||
builtin, | ||
location: createDummyLocation(), | ||
location: builtinLocation(), | ||
}; | ||
// buitlin namespaces don't have a cds file, so no location available | ||
function createDummyLocation() { | ||
return { | ||
filename: '<built-in>', | ||
start: { offset: 0, line: 1, column: 1 }, | ||
end: { offset: 0, line: 1, column: 1 }, | ||
}; | ||
} | ||
} | ||
@@ -121,0 +115,0 @@ |
@@ -60,3 +60,3 @@ // | ||
} | ||
// console.log('RUN:',refString(art), art.elements ? Object.keys(art.elements) : 0) | ||
// console.log('RUN:', art.name, art.elements ? Object.keys(art.elements) : 0) | ||
const chain = []; | ||
@@ -95,3 +95,3 @@ let target = art; | ||
runMembers( art ); | ||
// console.log('DONE:',refString(art), art.elements ? Object.keys(art.elements) : 0) | ||
// console.log('DONE:', art.name, art.elements ? Object.keys(art.elements) : 0) | ||
} | ||
@@ -160,3 +160,3 @@ | ||
function expensive( prop, target, source ) { | ||
// console.log(prop,refString(source),'->',target.kind,refString(target)); | ||
// console.log(prop,source.name,'->',target.kind,target.name); | ||
if (prop !== 'foreignKeys' && availableAtType( prop, target, source )) | ||
@@ -172,2 +172,3 @@ // foreignKeys must always be copied with target to avoid any confusion | ||
const dict = source[prop]; | ||
target[prop] = Object.create(null); // also propagate empty elements | ||
for (const name in dict) { | ||
@@ -174,0 +175,0 @@ const member = linkToOrigin( dict[name], name, target, prop, location ); |
@@ -46,3 +46,8 @@ // Compiler functions and utilities shared across all phases | ||
using: {}, | ||
extend: { isExtension: true, noDep: 'special' }, | ||
extend: { | ||
isExtension: true, | ||
noDep: 'special', | ||
elements: true, /* only for parse-cdl */ | ||
actions: true, /* only for parse-cdl */ | ||
}, | ||
annotate: { | ||
@@ -127,2 +132,3 @@ isExtension: true, noDep: 'special', elements: true, enum: true, actions: true, params: true, | ||
resolvePath, | ||
resolveTypeArguments, | ||
defineAnnotations, | ||
@@ -347,2 +353,38 @@ }; | ||
// Resolve the type arguments provided with a type referenced for artifact or | ||
// element `artifact`. This function does nothing if the referred type | ||
// `typeArtifact` does not have a `parameters` property (currently, only | ||
// builtin-types have it, see ./builtins.js). | ||
// | ||
// For each property name `<prop>` in `typeArtifact.parameters`, we move a number | ||
// in art.typeArguments (a vector of numbers with locations) to `artifact.<prop>`. | ||
// TODO: error if no parameters applicable | ||
// TODO: also check for number | ||
function resolveTypeArguments(artifact, typeArtifact, user) { | ||
const args = artifact.typeArguments || []; | ||
const parameters = typeArtifact.parameters || []; | ||
const parLength = parameters.length; | ||
for (let i = 0; i < parLength; ++i) { | ||
let par = parameters[i]; | ||
if (!(par instanceof Object)) | ||
par = { name: par }; | ||
if (!artifact[par.name] && (i < args.length || par.literal)) { | ||
const { location } = artifact.type; | ||
artifact[par.name] = args[i] || { | ||
literal: par.literal, val: par.val, location, $inferred: 'type-param', | ||
}; | ||
} | ||
} | ||
if (args.length > parLength) { | ||
artifact.typeArguments = artifact.typeArguments.slice(parLength); | ||
message( 'unexpected-type-arg', artifact.typeArguments[0].location, | ||
user, { art: typeArtifact }, | ||
'Warning', 'Too many arguments for type $(ART)' ); | ||
} | ||
else if (artifact.typeArguments) { | ||
delete artifact.typeArguments; | ||
} | ||
} | ||
// Set a cross-reference from the 'user' in artifact 'art' | ||
@@ -380,2 +422,4 @@ // 'user' is a navigatable node, while 'where' gives a closer hint (e.g. an item in a path) | ||
if (!spec.next && !extDict) { | ||
// CSN artifact paths are always fully qualified so we use | ||
// model.definitions for the JSON frontend. | ||
extDict = (spec.useDefinitions || env.$frontend && env.$frontend !== 'cdl') | ||
@@ -689,12 +733,2 @@ ? model.definitions | ||
// Create a location with location properties `filename` and `start` from | ||
// argument `start`, and location property `end` from argument `end`. | ||
function combinedLocation( start, end ) { | ||
return { | ||
filename: start.location.filename, | ||
start: start.location.start, | ||
end: end.location.end, | ||
}; | ||
} | ||
// Return string 'A.B.C' for parsed source `A.B.C` (is vector of ids with | ||
@@ -751,2 +785,4 @@ // locations): | ||
elem.name.absolute = elem._main.name.absolute; | ||
if (name == null) | ||
return; | ||
const normalized = kindProperties[elem.kind].normalized || elem.kind; | ||
@@ -802,3 +838,2 @@ [ 'element', 'alias', 'query', 'param', 'action' ].forEach( ( kind ) => { | ||
withAssociation, | ||
combinedLocation, | ||
}; |
@@ -187,3 +187,3 @@ 'use strict'; | ||
const { error, warning, info, signal } = alerts(csn); | ||
const { signal } = alerts(csn); | ||
@@ -257,3 +257,3 @@ // global variable where we store all the generated annotations | ||
} | ||
signal(warning`${fullMessage}`); | ||
signal(signal.warning`${fullMessage}`); | ||
} | ||
@@ -273,3 +273,3 @@ | ||
} | ||
signal(info`${fullMessage}`); | ||
signal(signal.info`${fullMessage}`); | ||
} | ||
@@ -410,3 +410,3 @@ | ||
let mapType = (p) => (p.type.startsWith('cds.') && !p.type.startsWith('cds.foundation.')) ? | ||
edmUtils.mapCdsToEdmType(p, signal, error, false /*is only called for v4*/) : p.type; | ||
edmUtils.mapCdsToEdmType(p, signal, false /*is only called for v4*/) : p.type; | ||
for (let n in action.params) { | ||
@@ -413,0 +413,0 @@ let p = action.params[n]; |
'use strict'; | ||
const { emptyLocation } = require('../../base/location'); | ||
// https://www.npmjs.com/package/sax | ||
@@ -7,7 +8,2 @@ const sax = require('sax'); | ||
const parser = sax.parser(true); | ||
let locationTemplate = { | ||
filename: null, | ||
start: { offset: 0, line: 0, column: 0 }, | ||
end: { offset: 0, line: 0, column: 0 } | ||
}; | ||
@@ -103,3 +99,2 @@ /* ====================================================== | ||
function parseXmlWithLocation(xml, filename) { | ||
locationTemplate.filename = filename || '<input>'; | ||
let result = Object.create(null); | ||
@@ -138,3 +133,3 @@ let currentNode = result; | ||
// setup location of the current node | ||
let loc = JSON.parse(JSON.stringify(locationTemplate)); | ||
let loc = emptyLocation(filename || '<input>'); | ||
setStartPosition(loc.start, parser); | ||
@@ -141,0 +136,0 @@ Object.defineProperty(currentNode, '_location', { value: loc, enumerable: false }); |
@@ -34,3 +34,3 @@ 'use strict'; | ||
const { error, warning, signal } = alerts(csn); | ||
const { signal } = alerts(csn); | ||
checkCSNVersion(csn, _options); | ||
@@ -47,3 +47,3 @@ | ||
if(services.length == 0) | ||
signal(error`No Services found in model`); | ||
signal(signal.error`No Services found in model`); | ||
@@ -53,3 +53,3 @@ if(serviceName) { | ||
if(serviceCsn == undefined) { | ||
signal(warning`No service definition with name "${serviceName}" found in the model`); | ||
signal(signal.warning`No service definition with name "${serviceName}" found in the model`); | ||
} | ||
@@ -143,3 +143,3 @@ else { | ||
let artifactName = `${Schema.Namespace}.${name}`; | ||
signal(error`Duplicate name "${name}" in Namespace "${Schema.Namespace}"`, ['definitions',artifactName]); | ||
signal(signal.error`Duplicate name "${name}" in Namespace "${Schema.Namespace}"`, ['definitions',artifactName]); | ||
} | ||
@@ -153,3 +153,3 @@ } | ||
if(Schema._children.length == 0) { | ||
signal(warning`Schema is empty`, ['definitions',Schema.Namespace]); | ||
signal(signal.warning`Schema is empty`, ['definitions',Schema.Namespace]); | ||
} | ||
@@ -170,6 +170,6 @@ | ||
if(properties.length === 0) { | ||
signal(error`EntityType "${serviceName}.${EntityTypeName}" has no properties`, ['definitions',entityCsn.name]); | ||
signal(signal.error`EntityType "${serviceName}.${EntityTypeName}" has no properties`, ['definitions',entityCsn.name]); | ||
} | ||
if(entityCsn.$edmKeyPaths.length === 0) { | ||
signal(error`EntityType "${serviceName}.${EntityTypeName}" has no primary key`, ['definitions',entityCsn.name]); | ||
signal(signal.error`EntityType "${serviceName}.${EntityTypeName}" has no primary key`, ['definitions',entityCsn.name]); | ||
} | ||
@@ -279,3 +279,3 @@ | ||
if(actionCsn.returns) { | ||
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns, fullQualified); | ||
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns); | ||
// if binding type matches return type add attribute EntitySetPath | ||
@@ -364,3 +364,3 @@ if(entityCsn != undefined && fullQualified(entityCsn.name) === actionNode._returnType._type) { | ||
if(type && isBuiltinType(type)) | ||
type = edmUtils.mapCdsToEdmType(returns, signal, error, options.isV2()); | ||
type = edmUtils.mapCdsToEdmType(returns, signal, options.isV2()); | ||
@@ -541,3 +541,3 @@ if(type && action.returns.items) | ||
reuseAssoc = !!forwardAssocCsn._NavigationProperty; | ||
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, warning, options.isFlatFormat); | ||
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, options.isFlatFormat); | ||
constraints._multiplicity = edmUtils.determineMultiplicity(forwardAssocCsn); | ||
@@ -544,0 +544,0 @@ } |
@@ -10,3 +10,3 @@ // @ts-nocheck | ||
const definitions = csn.definitions; | ||
const { error, info, signal } = alerts(csn, options); | ||
const { signal } = alerts(csn, options); | ||
@@ -568,41 +568,2 @@ class Node | ||
/* ReturnType is only used in v4, mapCdsToEdmType can be safely | ||
called with V2=false */ | ||
class ReturnType extends Node | ||
{ | ||
constructor(v, csn, fullQualified) | ||
{ | ||
let typecsn = csn.items || csn; | ||
let type = typecsn.type; | ||
if(type && type.startsWith('cds.') && !type.startsWith('cds.foundation.')) { | ||
type = edmUtils.mapCdsToEdmType(typecsn, signal, error, false, false); | ||
} | ||
else | ||
type = fullQualified(type); | ||
super(v, { Type: type }); | ||
edmUtils.addTypeFacets(this, csn); | ||
this.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true }); | ||
if(this._isCollection) | ||
this.Type = `Collection(${this.Type})` | ||
} | ||
// we need Name but NO $kind, can't use standard to JSON() | ||
toJSON() | ||
{ | ||
let rt = Object.create(null); | ||
// use the original type, not the decorated one | ||
if(this._type !== 'Edm.String') | ||
rt['$Type'] = this._type; | ||
if(this._isCollection) | ||
rt['$Collection'] = this._isCollection; | ||
// !this._nullable if Nullable=true become default | ||
if(this._nullable) | ||
rt['$Nullable'] = this._nullable; | ||
return rt; | ||
} | ||
} | ||
class TypeBase extends Node | ||
@@ -637,3 +598,3 @@ { | ||
if(scalarType) { | ||
this[typeName] = edmUtils.mapCdsToEdmType(scalarType, signal, error, this.v2, csn['@Core.MediaType']); | ||
this[typeName] = edmUtils.mapCdsToEdmType(scalarType, signal, this.v2, csn['@Core.MediaType']); | ||
// CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream | ||
@@ -660,3 +621,3 @@ if(this[typeName] !== 'Edm.Stream') | ||
} else if(odataType) { | ||
signal(info`@odata.Type: '${odataType}' is ignored, only 'Edm.String' and 'Edm.Int[16,32,64]' are allowed`, csn.$location); | ||
signal(signal.info`@odata.Type: '${odataType}' is ignored, only 'Edm.String' and 'Edm.Int[16,32,64]' are allowed`, csn.$location); | ||
} | ||
@@ -702,2 +663,28 @@ } | ||
/* ReturnType is only used in v4, mapCdsToEdmType can be safely | ||
called with V2=false */ | ||
class ReturnType extends TypeBase | ||
{ | ||
constructor(v, csn) | ||
{ | ||
super(v, {}, csn); | ||
/* all return types are nullable by default, | ||
CDS does not allow to specify not null | ||
*/ | ||
this.set( { _nullable: true }); | ||
} | ||
// we need Name but NO $kind, can't use standard to JSON() | ||
toJSON() | ||
{ | ||
let json = Object.create(null); | ||
this.toJSONattributes(json); | ||
// !this._nullable if Nullable=true become default | ||
if(this._nullable) | ||
json['$Nullable'] = this._nullable; | ||
return json; | ||
} | ||
} | ||
class ComplexType extends TypeBase { } | ||
@@ -704,0 +691,0 @@ class EntityType extends ComplexType |
@@ -37,3 +37,3 @@ 'use strict'; | ||
const { error, warning, info, signal } = alerts(csn); | ||
const { signal } = alerts(csn); | ||
const { | ||
@@ -52,3 +52,3 @@ getCsnDef, | ||
forAll(csn.definitions, (a, n) => { | ||
setProp (a, 'name', n); | ||
assignProp (a, 'name', n); | ||
}); | ||
@@ -122,3 +122,3 @@ | ||
else { | ||
signal(error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]); | ||
signal(signal.error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]); | ||
} | ||
@@ -214,5 +214,14 @@ } | ||
kind: 'entity', | ||
elements: Object.create(null) | ||
elements: Object.create(null), | ||
'@sap.semantics': 'parameters', | ||
}; | ||
setProp(parameterCsn, '$keys', Object.create(null)); | ||
/* | ||
<EntitySet Name="ZRHA_TEST_CDS" EntityType="ZRHA_TEST_CDS_CDS.ZRHA_TEST_CDSParameters" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:pageable="false" sap:content-version="1"/> | ||
*/ | ||
assignProp(parameterCsn, '_EntitySetAttributes', | ||
{'@sap.creatable': false, '@sap.updatable': false, '@sap.deletable': false, '@sap.pageable': false }); | ||
assignProp(parameterCsn, '$keys', Object.create(null)); | ||
setProp(parameterCsn, '$isParamEntity', true); | ||
@@ -265,2 +274,11 @@ | ||
} | ||
/* | ||
<EntitySet Name="ZRHA_TEST_CDSSet" EntityType="ZRHA_TEST_CDS_CDS.ZRHA_TEST_CDSType" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:addressable="false" sap:content-version="1"/> | ||
*/ | ||
assignProp(entityCsn, '_EntitySetAttributes', | ||
{'@sap.creatable': false, '@sap.updatable': false, '@sap.deletable': false, '@sap.addressable': false }); | ||
// redirect inbound associations/compositions to the parameter entity | ||
@@ -394,5 +412,6 @@ Object.keys(entityCsn.$sources || {}).forEach(n => { | ||
} | ||
setProp(struct, '_EntitySetAttributes', Object.create(null)); | ||
setProp(struct, '$keys', keys); | ||
assignProp(struct, '_EntitySetAttributes', Object.create(null)); | ||
assignProp(struct, '$keys', keys); | ||
applyAppSpecificLateCsnTransformationOnStructure(options, struct); | ||
@@ -439,3 +458,3 @@ | ||
if (element._ignore) return; | ||
setProp(element, '_constraints', getReferentialConstraints(element, signal, warning, options.isFlatFormat)); | ||
setProp(element, '_constraints', getReferentialConstraints(element, signal, options.isFlatFormat)); | ||
@@ -450,3 +469,3 @@ // only in V2 we must set the target cardinality of the backlink to the forward: | ||
// Association 'E_toF': Multiplicity of Role='E' defined to '*', conflicting with target multiplicity '0..1' from | ||
signal(warning`Source cardinality "${element._constraints._originAssocCsn.cardinality.src}" of "${element._constraints._originAssocCsn._parent.name}/${element._constraints._originAssocCsn.name}" conflicts with target cardinality "${element.cardinality.max}" of association "${element._parent.name}/${element.name}"`); | ||
signal(signal.warning`Source cardinality "${element._constraints._originAssocCsn.cardinality.src}" of "${element._constraints._originAssocCsn._parent.name}/${element._constraints._originAssocCsn.name}" conflicts with target cardinality "${element.cardinality.max}" of association "${element._parent.name}/${element.name}"`); | ||
} | ||
@@ -490,3 +509,3 @@ } | ||
element._ignore = true; | ||
signal(info`${element.type.replace('cds.', '')} "${element.name}" excluded, target "${element._target.name}" is annotated '@cds.autoexpose: ${element._target['@cds.autoexpose']}'`, | ||
signal(signal.info`${element.type.replace('cds.', '')} "${element.name}" excluded, target "${element._target.name}" is annotated '@cds.autoexpose: ${element._target['@cds.autoexpose']}'`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -537,3 +556,3 @@ return; | ||
else { | ||
signal(info`Unmanaged associations not supported as primary keys of proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
signal(signal.info`Unmanaged associations not supported as primary keys of proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -556,3 +575,3 @@ } | ||
element._ignore = true; | ||
signal(info`Could not create proxy entity type "${name}" for unexposed association target "${element._target.name}", because target has no primary keys`, | ||
signal(signal.info`Could not create proxy entity type "${name}" for unexposed association target "${element._target.name}", because target has no primary keys`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -563,6 +582,6 @@ return; | ||
if(csn.definitions[name] !== undefined) | ||
signal(error`Duplicate proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
signal(signal.error`Duplicate proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
else { | ||
signal(info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
signal(signal.info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -579,3 +598,3 @@ csn.definitions[name] = proxy; | ||
element._ignore = true; | ||
signal(warning`No OData navigation property generated for association "${element.name}", as target "${element._target.name}" is outside any service`, | ||
signal(signal.warning`No OData navigation property generated for association "${element.name}", as target "${element._target.name}" is outside any service`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -632,3 +651,3 @@ return; | ||
if (type) { | ||
signal(error`Cannot create type "${typeName}" for "${parentName}" because the name is already used`); | ||
signal(signal.error`Cannot create type "${typeName}" for "${parentName}" because the name is already used`); | ||
return undefined; | ||
@@ -698,3 +717,3 @@ } | ||
let annos = Object.keys(eltCsn).filter(a=>a[0]==='@').join(', '); | ||
signal(warning`${struct.name}: OData V4 primary key path: "${prefix}" is unexposed by one of these annotations "${annos}"`, ['definitions', struct.name, 'elements', eltCsn.name ]); | ||
signal(signal.warning`${struct.name}: OData V4 primary key path: "${prefix}" is unexposed by one of these annotations "${annos}"`, ['definitions', struct.name, 'elements', eltCsn.name ]); | ||
return keyPaths; | ||
@@ -1024,4 +1043,11 @@ } | ||
// Set non enumerable property if it doesn't exist yet | ||
function assignProp(obj, prop, value) { | ||
if(obj[prop] === undefined) { | ||
setProp(obj, prop, value); | ||
} | ||
} | ||
module.exports = { | ||
initializeModel, | ||
} |
@@ -152,3 +152,3 @@ 'use strict'; | ||
function getReferentialConstraints(assocCsn, signal, warning, isFlatFormat) | ||
function getReferentialConstraints(assocCsn, signal, isFlatFormat) | ||
{ | ||
@@ -185,3 +185,3 @@ let result = { constraints: Object.create(null), selfs: [], termCount: 0 }; | ||
isBacklink = false; | ||
signal(warning`"${assocCsn._parent.name}/${assocCsn.name}" references "${originAssocCsn._parent.name}/${partner}" in $self ON condition with target "${originAssocCsn._target.name}"`, ['definitions', parentArtifactName]); | ||
signal(signal.info`"${assocCsn._parent.name}/${assocCsn.name}" references "${originAssocCsn._parent.name}/${partner}" with target "${originAssocCsn._target.name}" in ON condition with $self`, ['definitions', parentArtifactName]); | ||
} | ||
@@ -225,7 +225,7 @@ if(isAssociationOrComposition(originAssocCsn)) { | ||
{ | ||
signal(warning`Cannot resolve backlink to ${assocCsn._target.name}/${partner}" from "${parentArtifactName}/${assocCsn.name}"`, ['definitions', parentArtifactName]); | ||
signal(signal.warning`Cannot resolve backlink to ${assocCsn._target.name}/${partner}" from "${parentArtifactName}/${assocCsn.name}"`, ['definitions', parentArtifactName]); | ||
} | ||
} | ||
else { | ||
signal(warning`Could not resolve partner association from "$self" expression`, ['definitions', assocCsn._parent.name, 'elements', assocCsn.name]); | ||
signal(signal.warning`Could not resolve partner association from "$self" expression`, ['definitions', assocCsn._parent.name, 'elements', assocCsn.name]); | ||
@@ -422,7 +422,7 @@ } | ||
function mapCdsToEdmType(csn, signal, error, isV2=false, isMediaType=false) | ||
function mapCdsToEdmType(csn, signal, isV2=false, isMediaType=false) | ||
{ | ||
let cdsType = csn.type; | ||
if(cdsType === undefined) { | ||
signal(error`no type found`, csn.$location); | ||
signal(signal.error`no type found`, csn.$location); | ||
return '<NOTYPE>'; | ||
@@ -479,3 +479,3 @@ } | ||
if (edmType == undefined) { | ||
signal(error`No Edm type available for "${cdsType}"`, csn.$location); | ||
signal(signal.error`No Edm type available for "${cdsType}"`, csn.$location); | ||
} | ||
@@ -489,3 +489,3 @@ if(isV2) | ||
if(['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(cdsType)) { | ||
signal(error`"OData V2 does not support Geometry data types, ${cdsType}" cannot be mapped`, csn.$location); | ||
signal(signal.error`"OData V2 does not support Geometry data types, ${cdsType}" cannot be mapped`, csn.$location); | ||
} | ||
@@ -492,0 +492,0 @@ if(cdsType === 'cds.DecimalFloat' || cdsType === 'cds.hana.SMALLDECIMAL') |
@@ -84,3 +84,4 @@ // CSN frontend - transform CSN into XSN | ||
const { normalizeLocation, getMessageFunction } = require('../base/messages'); | ||
const { normalizeLocation } = require('../base/location'); | ||
const { getMessageFunction } = require('../base/messages'); | ||
const { addToDict } = require('../base/dictionaries'); | ||
@@ -139,2 +140,5 @@ | ||
const schema = compileSchema( { | ||
requires: { | ||
type: renameTo( 'dependencies', arrayOf( stringVal, val => (val.literal === 'string') ) ), | ||
}, | ||
// definitions: ------------------------------------------------------------ | ||
@@ -178,3 +182,3 @@ definitions: { | ||
validKinds: [ 'action', 'function' ], | ||
inKind: [ 'entity', 'annotate' ], | ||
inKind: [ 'entity', 'annotate', 'extend' ], | ||
}, | ||
@@ -196,2 +200,3 @@ params: { | ||
requires: [ 'ref', 'xpr', 'val', '#', 'func', 'SELECT', 'SET' ], // requires one of... | ||
inKind: [ 'extend' ], // only valid in extend and SELECT | ||
}, | ||
@@ -517,3 +522,3 @@ keys: { | ||
arrayOf: stringRef, | ||
inKind: [ 'entity', 'type' ], | ||
inKind: [ 'entity', 'type', 'extend' ], | ||
}, | ||
@@ -578,3 +583,3 @@ returns: { | ||
type: object, | ||
optional: [ 'definitions', 'extensions', 'namespace', 'version', 'messages', 'meta', 'options', '@' ], | ||
optional: [ 'requires', 'definitions', 'extensions', 'namespace', 'version', 'messages', 'meta', 'options', '@' ], | ||
requires: false, // empty object OK | ||
@@ -601,2 +606,3 @@ schema, | ||
let virtualLine = 1; | ||
/** @type {XSN.Location[]} */ | ||
let dollarLocations = []; | ||
@@ -659,3 +665,3 @@ | ||
function arrayOf( fn ) { | ||
function arrayOf( fn, filter = undefined ) { | ||
return function arrayMap( val, spec, xsn, csn ) { | ||
@@ -679,2 +685,4 @@ if (!isArray( val, spec )) | ||
++virtualLine; // [] in one JSON line | ||
if (filter) | ||
return r.filter(filter); | ||
return r; | ||
@@ -774,2 +782,3 @@ }; | ||
const csnProps = Object.keys( def ); | ||
if (csnProps.length) { | ||
@@ -951,2 +960,3 @@ const valueName = (prop === 'keys' || prop === 'foreignKeys' ? 'targetElement' : 'value'); | ||
} | ||
function stringVal( val, spec ) { | ||
@@ -1478,2 +1488,3 @@ if (typeof val === 'string' && val) | ||
} | ||
/** @type {XSN.Location} */ | ||
const loc = { | ||
@@ -1480,0 +1491,0 @@ filename, start: { line, column }, end: { line, column }, $weak: true, |
@@ -116,2 +116,3 @@ // Transform augmented CSN into compact "official" CSN | ||
_typeIsExplicit: ignore, | ||
expectedKind: ignore, // TODO: may be set in extensions but is unused | ||
}; | ||
@@ -220,6 +221,18 @@ | ||
/** | ||
* Compact the given XSN model and transform it into CSN. | ||
* | ||
* @param {XSN.Model} model | ||
* @param {CSN.Options} options | ||
* @returns {CSN.Model} | ||
*/ | ||
function compactModel( model, options = model.options || {} ) { | ||
gensrcFlavor = options.toCsn && options.toCsn.flavor === 'gensrc'; | ||
gensrcFlavor = options.parseCdl || options.toCsn && options.toCsn.flavor === 'gensrc'; | ||
strictMode = options.testMode; | ||
const csn = {}; | ||
if (options.parseCdl) { | ||
const using = usings( model.sources || {} ); | ||
if (using.length) | ||
csn.requires = using; | ||
} | ||
set( 'definitions', csn, model ); | ||
@@ -254,2 +267,27 @@ const exts = extensions( model.extensions || [], csn, model ); | ||
/** | ||
* Create a CSN `requires` array of dependencies. | ||
* | ||
* @param {object} sources Dictionary of source files to their AST/XSN. | ||
*/ | ||
function usings( sources ) { | ||
const sourceNames = Object.keys(sources); | ||
if (sourceNames.length === 0) | ||
return []; | ||
// Take the first file as parseCdl should only receive one file. | ||
const source = sources[sourceNames[0]]; | ||
const requires = []; | ||
if (source && source.dependencies) | ||
source.dependencies.map(dep => dep && requires.push(dep.val)); | ||
// Make unique and sort | ||
return Array.from(new Set(requires)).sort(); | ||
} | ||
/** | ||
* @param {XSN.Extension[]} node | ||
* @param {object} csn | ||
* @param {object} model | ||
*/ | ||
function extensions( node, csn, model ) { | ||
@@ -487,12 +525,12 @@ const exts = node.map( standard ).sort( | ||
if (node.scope === 'typeOf') { // TYPE OF without ':' in path | ||
// Can only be rendered with root _artifact which is an element | ||
// In CDL->CSN transformation, _artifact for first item in ref for TYPE OF | ||
// <elem> should be set to { _parent: <current user of TYPE OF> } | ||
// Root _artifact which is either element or main artifact for paths starting with $self. | ||
// To make the CDL->CSN transformation simpler, the _artifact for first item could be | ||
// a fake element with just a correct absolute name and _parent/_main links. | ||
if (!root._main || root.kind === 'query') { // $self/$projection | ||
// in query, only correct for leading query -> | ||
// TODO: forbid TYPE OF elem / TYPE OF $self.elem in queries | ||
return renderArtifactPath( [ { id: absolute }, ...path.slice(1) ], terse ); | ||
} | ||
const parent = root._parent; | ||
if (!parent) { | ||
if (strictMode) | ||
throw new Error( `Illegal artifact link in ${ locationString(node.location) }`); | ||
return renderArtifactPath( path, terse ); // is wrong anyway | ||
} | ||
const structs = parent._main ? parent.name.element.split('.') : []; | ||
const structs = parent.name.element ? parent.name.element.split('.') : []; | ||
return { ref: [ absolute, ...structs, ...path.map( pathItem ) ] }; | ||
@@ -574,3 +612,5 @@ } | ||
function enumValue( v, csn, node ) { | ||
if (node.kind === 'enum') | ||
// Enums can have values but if enums are extended, their kind is 'element', | ||
// so we check whether the node is inside an extension. | ||
if (node.kind === 'enum' || node._parent && node._parent.kind === 'extend') | ||
Object.assign( csn, expression(v) ); | ||
@@ -880,3 +920,4 @@ } | ||
function addExplicitAs( node, name, implicit ) { | ||
if (name && (!name.calculated && !name.$inferred || !node.ref || implicit && implicit(name.id) )) | ||
if (name && name.id && | ||
(!name.calculated && !name.$inferred || !node.ref || implicit && implicit(name.id) )) | ||
node.as = name.id; | ||
@@ -883,0 +924,0 @@ return node; |
@@ -10,6 +10,8 @@ 'use strict'; | ||
* Must be a valid doc comment. | ||
* @param {boolean} [normalizeLineBreak=false] Whether to normalize line breaks, i.e. replace | ||
* `\r\n` with `\n`. Useful for test mode. | ||
* @returns {string|null} Parsed contents or if the comment has an invalid format or | ||
* does not have any content, null is returned. | ||
*/ | ||
function parseDocComment(comment) { | ||
function parseDocComment(comment, normalizeLineBreak = false) { | ||
// Also return "null" for empty doc comments so that doc comment propagation | ||
@@ -20,2 +22,5 @@ // can be stopped. | ||
if (normalizeLineBreak) | ||
comment = comment.replace(/\r\n/g, '\n'); | ||
let lines = comment.split('\n'); | ||
@@ -22,0 +27,0 @@ |
@@ -10,3 +10,3 @@ // Generic ANTLR parser class with AST-building functions | ||
var { addToDictWithIndexNo } = require('../base/dictionaries'); | ||
const locUtils = require('../base/location'); | ||
const { parseDocComment } = require('./docCommentParser'); | ||
@@ -209,4 +209,8 @@ | ||
// Return start location of `token`, or the first token matched by the current | ||
// rule if `token` is undefined | ||
/** | ||
* Return start location of `token`, or the first token matched by the current | ||
* rule if `token` is undefined | ||
* | ||
* @returns {XSN.Location} | ||
*/ | ||
function startLocation( token = this._ctx.start ) { | ||
@@ -243,7 +247,3 @@ return { | ||
start = { location: this.startLocation() }; | ||
return { | ||
filename: start.location.filename, | ||
start: start.location.start, | ||
end: end && end.location && end.location.end | ||
}; | ||
return locUtils.combinedLocation( start, end ); | ||
} | ||
@@ -264,3 +264,4 @@ | ||
} | ||
node.doc = this.tokenLocation( token, token, parseDocComment( token.text ) ); | ||
const normalizeLineBreak = !!this.options.testMode; | ||
node.doc = this.tokenLocation( token, token, parseDocComment( token.text, normalizeLineBreak ) ); | ||
} | ||
@@ -267,0 +268,0 @@ |
@@ -18,4 +18,4 @@ // Main entry point for the Research Vanilla CDS Compiler | ||
const { odata, cdl, sql, hdi, hdbcds, edm, edmx } = require('./api/main'); | ||
const { emptyWeakLocation } = require('./base/location'); | ||
// The compiler version (taken from package.json) | ||
@@ -74,3 +74,3 @@ function version() { | ||
message( 'file-unknown-ext', | ||
{ filename, start: { offset: 0, line: 1, column: 1 } }, null, | ||
emptyWeakLocation(filename), null, | ||
{ file: ext && ext.slice(1), '#': !ext && 'none' }, | ||
@@ -145,3 +145,3 @@ 'Error', { | ||
}); | ||
if (!options.parseOnly) | ||
if (!options.parseOnly && !options.parseCdl) | ||
all = all.then( readDependencies ); | ||
@@ -502,2 +502,7 @@ return all.then( function() { | ||
define( model ); | ||
// do not run the resolver in parse-cdl mode or we get duplicate annotations, etc. | ||
if (options.parseCdl) | ||
return handleMessages( model ); | ||
resolve( model ); | ||
@@ -504,0 +509,0 @@ assertConsistency( model ); |
@@ -11,3 +11,3 @@ 'use strict' | ||
* Get utility functions for a given CSN. | ||
* @param model - (compact) CSN model | ||
* @param {CSN.Model} model (Compact) CSN model | ||
*/ | ||
@@ -34,9 +34,9 @@ function getUtils(model) { | ||
getFinalBaseType, | ||
inspectRef | ||
inspectRef, | ||
artifactRef | ||
}; | ||
/** | ||
* Create an object to track visited objects identified | ||
* by a unique sring. | ||
* @param id - initial entry (optional) | ||
* Create an object to track visited objects identified by a unique string. | ||
* @param {string} [id] Initial entry (optional) | ||
*/ | ||
@@ -51,3 +51,3 @@ function createVisited(id) { | ||
* add it to the list of visited identifiers. | ||
* @param id - unique identifier | ||
* @param {string} id unique identifier | ||
*/ | ||
@@ -65,3 +65,3 @@ function check(id) { | ||
* Get the CSN definition for an artifact name. | ||
* @param defName - absolute name of the artifact | ||
* @param {string} defName Absolute name of the artifact | ||
*/ | ||
@@ -76,5 +76,6 @@ function getCsnDef(defName) { | ||
/** | ||
* Returns if an artifact is a structured type | ||
* Returns true if an artifact is a structured type | ||
* or a typedef of a structured type. | ||
* @param obj - artifact | ||
* | ||
* @param {CSN.Artifact} obj | ||
*/ | ||
@@ -89,3 +90,5 @@ function isStructured(obj) { | ||
* If the artifact for typename isn't a typedef, the name itself is returned. | ||
* @param typeName - absolute name | ||
* | ||
* @param {string} typeName Absolute type name | ||
* @returns {object} | ||
*/ | ||
@@ -108,3 +111,4 @@ function getFinalTypeDef(typeName) { | ||
* Resolves typedefs to its final type (name) which is returned. | ||
* @param typeName - absolute name | ||
* @param {string} typeName Absolute type name | ||
* @returns {string} | ||
*/ | ||
@@ -134,3 +138,3 @@ function getFinalType(typeName) { | ||
function isManagedAssociationElement(node) { | ||
return node.target != undefined && node.on == undefined; | ||
return node.target !== undefined && node.on === undefined; | ||
} | ||
@@ -141,3 +145,3 @@ | ||
* to any of them. | ||
* @param typeName - absolute type name | ||
* @param {string} typeName Absolute type name | ||
*/ | ||
@@ -160,3 +164,3 @@ function isAssocOrComposition(typeName) { | ||
* Returns if a type is an association or a typedef to it. | ||
* @param typeName - absolute type name | ||
* @param {string} typeName Absolute type name | ||
*/ | ||
@@ -179,3 +183,3 @@ function isAssociation(typeName) { | ||
* Returns if a type is an composition or a typedef to it. | ||
* @param typeName - absolute type name | ||
* @param {string} typeName Absolute type name | ||
*/ | ||
@@ -223,3 +227,3 @@ function isComposition(typeName) { | ||
* Return the namespace part of the artifact name. | ||
* @param name - absolute name of artifact | ||
* @param {string} name Absolute name of artifact | ||
*/ | ||
@@ -240,3 +244,3 @@ function getNamespaceOfArtifact(name) { | ||
* | ||
* @param {any} absoluteName Name of the annotation, including the at-sign | ||
* @param {string} absoluteName Name of the annotation, including the at-sign | ||
* @param {any} theValue string value of the annotation | ||
@@ -260,3 +264,5 @@ * @param {any} node Node to add the annotation to | ||
* Returns null if the artifact doesn't live in a service. | ||
* @param artifactName - absolute name of artifact | ||
* | ||
* @param {string} artifactName Absolute name of artifact | ||
* @returns {string|null} | ||
*/ | ||
@@ -413,3 +419,6 @@ function getServiceName(artifactName) { | ||
/** | ||
* Deeply clone the given CSN model and return it. | ||
* @param {CSN.Model} csn | ||
*/ | ||
function cloneCsn(csn){ | ||
@@ -425,5 +434,11 @@ // cannot require this earlier because to-csn has a dependency on this file | ||
// Apply function `callback` to all artifacts in dictionary | ||
// `model.definitions`. See function `forEachGeneric` for details. | ||
// callback will be called with artifact, artifact name, csn-path to artifact | ||
/** | ||
* Apply function `callback` to all artifacts in dictionary | ||
* `model.definitions`. See function `forEachGeneric` for details. | ||
* Callback will be called with artifact, artifact name, property | ||
* name ('definitions') and csn-path to artifact. | ||
* | ||
* @param {CSN.Model} csn | ||
* @param {(art: CSN.Artifact, name: string, prop: string, path: string[]) => any} callback | ||
*/ | ||
function forEachDefinition( csn, callback ) { | ||
@@ -433,7 +448,14 @@ forEachGeneric( csn, 'definitions', callback ); | ||
// Apply function `callback` to all members of object `obj` (main artifact or | ||
// parent member). Members are considered those in dictionaries `elements`, | ||
// `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also | ||
// searched inside property `items` (array of). See function `forEachGeneric` | ||
// for details. | ||
/** | ||
* Apply function `callback` to all members of object `obj` (main artifact or | ||
* parent member). Members are considered those in dictionaries `elements`, | ||
* `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also | ||
* searched inside property `items` (array of). See function `forEachGeneric` | ||
* for details. | ||
* | ||
* @param {CSN.Artifact} construct | ||
* @param {(art: CSN.Member, name: string, prop: string, path: string[]) => any} callback | ||
* @param {string[]} [path] | ||
* @param {boolean} [ignoreIgnore] | ||
*/ | ||
function forEachMember( construct, callback, path=[], ignoreIgnore=true) { | ||
@@ -465,4 +487,11 @@ let obj = construct.returns || construct; // why the extra `returns` for actions? | ||
// Apply function `callback(member, memberName)` to each member in `construct`, | ||
// recursively (i.e. also for sub-elements of elements). | ||
/** | ||
* Apply function `callback(member, memberName)` to each member in `construct`, | ||
* recursively (i.e. also for sub-elements of elements). | ||
* | ||
* @param {CSN.Artifact} construct | ||
* @param {(art: CSN.Member, name: string, prop: string, path: string[], origConstruct: CSN.Artifact) => any} callback | ||
* @param {CSN.Path} [path] | ||
* @param {boolean} [ignoreIgnore] | ||
*/ | ||
function forEachMemberRecursively( construct, callback, path=[], ignoreIgnore=true) { | ||
@@ -476,6 +505,13 @@ forEachMember( construct, ( member, memberName, prop, subpath ) => { | ||
// Apply function `callback` to all objects in dictionary `dict`, including all | ||
// duplicates (found under the same name). Function `callback` is called with | ||
// the following arguments: the object, the name, and -if it is a duplicate- | ||
// the array index and the array containing all duplicates. | ||
/** | ||
* Apply function `callback` to all objects in dictionary `dict`, including all | ||
* duplicates (found under the same name). Function `callback` is called with | ||
* the following arguments: the object, the name, and -if it is a duplicate- | ||
* the array index and the array containing all duplicates. | ||
* | ||
* @param {object} obj | ||
* @param {string} prop | ||
* @param {(dict: object, name: string, prop: string, path: CSN.Path) => any} callback | ||
* @param {CSN.Path} path | ||
*/ | ||
function forEachGeneric( obj, prop, callback, path = []) { | ||
@@ -492,3 +528,9 @@ let dict = obj[prop]; | ||
// For each property named 'ref' in 'node' (recursively), call callback(ref, node, path) | ||
/** | ||
* For each property named 'ref' in 'node' (recursively), call callback(ref, node, path) | ||
* | ||
* @param {object} node | ||
* @param {(ref: any, node: object, path: CSN.Path) => any} callback | ||
* @param {CSN.Path} path | ||
*/ | ||
function forEachRef(node, callback, path = []) { | ||
@@ -540,2 +582,7 @@ if (node == null || typeof node !== 'object') { | ||
/** | ||
* @param {CSN.Query} query | ||
* @param {(query: CSN.Query, path: CSN.Path) => any} callback | ||
* @param {CSN.Path} path | ||
*/ | ||
function forAllQueries(query, callback, path = []){ | ||
@@ -572,2 +619,7 @@ return traverseQuery(query, callback, path); | ||
/** | ||
* @param {CSN.QueryFrom} from | ||
* @param {Function} callback | ||
* @param {CSN.Path} path | ||
*/ | ||
function traverseFrom( from, callback, path = [] ) { | ||
@@ -588,5 +640,7 @@ if (from.ref) // ignore | ||
* Returns true if the element has a specific annotation set to the given value. | ||
* @param artifact - the artifact object | ||
* @param annotationName - the name of the annotation (including the at-sign) | ||
* @param value - the value | ||
* | ||
* @param {CSN.Artifact} artifact | ||
* @param {string} annotationName Name of the annotation (including the at-sign) | ||
* @param {any} value | ||
* @returns {boolean} | ||
*/ | ||
@@ -597,6 +651,11 @@ function hasBoolAnnotation(artifact, annotationName, value = true) { | ||
// EDM specific check: Render ordinary property if element is NOT ... | ||
// 1) ... annotated @cds.api.ignore | ||
// 2) ... annotated @odata.foreignKey4 and odataFormat: structured | ||
// function accepts EDM internal and external options | ||
/** | ||
* EDM specific check: Render ordinary property if element is NOT ... | ||
* 1) ... annotated @cds.api.ignore | ||
* 2) ... annotated @odata.foreignKey4 and odataFormat: structured | ||
* function accepts EDM internal and external options | ||
* | ||
* @param {CSN.Element} elementCsn | ||
* @param {CSN.Options} options | ||
*/ | ||
function isEdmPropertyRendered(elementCsn, options) { | ||
@@ -609,3 +668,2 @@ let isStructuredFormat = options.toOdata && options.toOdata.odataFormat === 'structured' || options.isStructFormat; | ||
module.exports = { | ||
@@ -612,0 +670,0 @@ getUtils, |
@@ -86,10 +86,9 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs | ||
function simpleRef( node, prop ) { | ||
let ref = node[prop]; | ||
if (typeof ref === 'string') { | ||
const art = artifactRef( ref, null ); | ||
if (art || !ref.startsWith( 'cds.')) | ||
node['_' + prop] = refLocation( art ); | ||
const notFound = (options.testMode) ? undefined : null; | ||
const ref = node[prop]; | ||
if (Array.isArray( ref )) { | ||
node['_' + prop] = ref.map( r => refLocation( artifactRef( r, notFound ) ) ); | ||
} | ||
else if (Array.isArray( ref )) { | ||
node['_' + prop] = ref.map( r => refLocation( artifactRef( r, null ) ) ); | ||
else if (typeof ref !== 'string' || !ref.startsWith( 'cds.')) { | ||
node['_' + prop] = refLocation( artifactRef( ref, notFound ) ); | ||
} | ||
@@ -96,0 +95,0 @@ // catch (e) { |
@@ -125,2 +125,21 @@ 'use strict' | ||
/** | ||
* Check whether the given artifact has type information. An artifact has type | ||
* information when it is either a builtin, a struct, an enum, an array, an | ||
* association OR if it references another type, i.e. typeOf. For the latter | ||
* case an artifact's final type must be checked. | ||
* | ||
* @param {object} artifact | ||
* @returns {boolean} | ||
*/ | ||
function hasArtifactTypeInformation(artifact) { | ||
// When is what property set? | ||
return artifact.builtin // => `Integer` | ||
|| artifact.elements // => `type A {}` | ||
|| artifact.items // => `type A : array of Integer` | ||
|| artifact.enum // => `type A : Integer enum {}`, `type` also set | ||
|| artifact.target // => `type A : Association to B;` | ||
|| artifact.type; // => `type A : [type of] Integer` | ||
} | ||
// Produce a printable name (for error messages) for element or artifact 'node' | ||
@@ -492,2 +511,3 @@ function printableName(node) { | ||
isContainerArtifact, | ||
hasArtifactTypeInformation, | ||
printableName, | ||
@@ -494,0 +514,0 @@ addStringAnnotationTo, |
@@ -80,4 +80,45 @@ // Make internal properties of the XSN / augmented CSN visible | ||
} | ||
return reveal( name && name !== '+' ? { definitions: model.definitions[name] } : model ); | ||
return reveal( parseXsnPath(name, model) ); | ||
// Returns the desired artifact/dictionary in the XSN. | ||
// | ||
// Usage: | ||
// 1. Whole Model | ||
// Simply pass `+`. | ||
// 2. Entity (e.g. in service) | ||
// Use `S.E`, i.e. the artifact's name in XSN. | ||
// 3. Specific Element | ||
// To get an element `e` of `S.E`, use `S.E/elements/e`, i.e. the | ||
// JSON path delimited by "/" instead of "." (to avoid conflicts with artifact's FQN). | ||
// 4. All elements | ||
// To list all elements, use `S.E/elements/`. The final slash is important. | ||
// 5. Other dictionaries or internal properties | ||
// Use the JSON-like path delimited by "/". Add a final slash, e.g. `E.elements.a.kind/`. | ||
// | ||
// The string before the last slash ("/") is used as the property name to | ||
// reveal the properties. So if the last path segment is an element name, do | ||
// not add a slash or the name may be mistaken as a property name. | ||
// | ||
// Examples: | ||
// `name.space/S/E/elements/a/kind/` | ||
// `name.space/S/E/elements/a/type/scope/` | ||
function parseXsnPath(path, xsn) { | ||
if (!path || path === '+') | ||
return xsn; | ||
path = path.split('/'); | ||
path.unshift('definitions'); | ||
for (const segment of path) { | ||
if (xsn[segment]) | ||
xsn = xsn[segment] | ||
else if (segment) | ||
throw new Error(`Raw Output: Path segment "${ segment }" could not be found. Path: ${ JSON.stringify(path) }!"`) | ||
} | ||
const propName = path[path.length > 1 ? path.length - 2 : 0 ]; | ||
const obj = {}; | ||
obj[propName] = xsn; | ||
return obj; | ||
} | ||
function artifactIdentifier( node, parent ) { | ||
@@ -102,4 +143,10 @@ if (node instanceof Array) | ||
return '$magicVariables'; | ||
if (!node.name) | ||
return JSON.stringify(node); | ||
if (!node.name) { | ||
try{ | ||
return JSON.stringify(node); | ||
} | ||
catch (e) { | ||
return e.toString(); | ||
} | ||
} | ||
switch (node.kind) { | ||
@@ -181,3 +228,4 @@ case undefined: // TODO: remove this `returns` property for actions | ||
function columns( nodes ) { | ||
return nodes && nodes.map( c => (c._parent) ? artifactIdentifier( c ) : reveal( c ) ); | ||
// If we will have specified elements, we need another test to see columns in --parse-cdl | ||
return nodes && nodes.map( c => (c._parent && c._parent.elements) ? artifactIdentifier( c ) : reveal( c ) ); | ||
} | ||
@@ -184,0 +232,0 @@ |
@@ -26,4 +26,4 @@ const { createOptionProcessor } = require('./base/optionProcessorHelper'); | ||
.option(' --beta-mode') | ||
.option(' --beta <list>') | ||
.option(' --old-transformers') | ||
.option(' --std-json-parser') | ||
.option(' --long-autoexposed') | ||
@@ -80,5 +80,5 @@ .option(' --hana-flavor') | ||
--internal-msg Write raw messages with call stack to <stdout>/<stderr> | ||
--beta-mode Enable unsupported, incomplete (beta) features | ||
--beta-mode Enable all unsupported, incomplete (beta) features | ||
--beta <list> Comma separated list of unsupported, incomplete (beta) features to use | ||
--old-transformers Use the old transformers that work on XSN instead of CSN | ||
--std-json-parser Use standard JSON parser for CSN parsing with old CSN frontend | ||
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete) | ||
@@ -89,3 +89,3 @@ --parse-only Stop compilation after parsing and write result to <stdout> | ||
--doc-comment Preserve /** */ comments at annotation positions as doc property in CSN | ||
Backward compatibility options (deprecated, do not use) | ||
@@ -101,2 +101,3 @@ --long-autoexposed Produce long names (with underscores) for autoexposed entities | ||
toCsn [options] <files...> (default) Generate original model as CSN | ||
parseCdl Generate a CSN that is close to the CDL source. | ||
toRename [options] <files...> (internal) Generate SQL DDL rename statements | ||
@@ -216,3 +217,4 @@ `); | ||
.option('-c, --csn') | ||
.help(` | ||
.option('--compatibility') | ||
.help(` | ||
Usage: cdsc toSql [options] <files...> | ||
@@ -253,2 +255,3 @@ | ||
-c, --csn Generate "sql_csn.json" with SQL-preprocessed model | ||
--compatibility Try and create ON-conditions just like HDBCDS | ||
`); | ||
@@ -295,4 +298,16 @@ | ||
optionProcessor.command('parseCdl') | ||
.option('-h, --help') | ||
.help(` | ||
Usage: cdsc parseCdl [options] <file> | ||
Only parse the CDL and output a CSN that is close to the source. Does not | ||
resolve imports, apply extensions or expand any queries. | ||
Options | ||
-h, --help Show this help text | ||
`); | ||
module.exports = { | ||
optionProcessor | ||
}; |
@@ -83,2 +83,3 @@ | ||
const { error, signal, warning, info } = alerts(csn, options); | ||
const compatMode = options.toSql && options.toSql.compatibility && options.toSql.dialect === 'hana' && options.toSql.src === 'hdi'; | ||
@@ -209,2 +210,3 @@ // FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect | ||
let childEnv = increaseIndent(env); | ||
childEnv.isEntity = true; | ||
let hanaTc = art.technicalConfig && art.technicalConfig.hana; | ||
@@ -241,3 +243,3 @@ let result = ''; | ||
.join(', '); | ||
if (uniqueFields !== '') { | ||
if (uniqueFields !== '' && !compatMode) { | ||
result += ',\n' + childEnv.indent + 'UNIQUE(' + uniqueFields + ')' | ||
@@ -328,4 +330,8 @@ } | ||
result += ' JOIN '; | ||
result += quoteSqlId(absoluteCdsName(elm.target)) + ' AS ' + quoteSqlId(elementName) + ' ON ('; | ||
result += renderExpr(elm.on, env) + ')'; | ||
result += quoteSqlId(absoluteCdsName(elm.target)) + ' AS ' + quoteSqlId(elementName) + ` ON ${compatMode ? '' : '('}`; | ||
if(compatMode && env.isEntity){ | ||
result += renderHdbcdsConformingOnCondition(prepOnCondition(elm.on), env); | ||
} else { | ||
result += renderExpr(elm.on, env) + ')'; | ||
} | ||
} | ||
@@ -548,3 +554,3 @@ return result; | ||
result += env.indent + 'NULL AS ' + quoteSqlId(col.as || leaf); | ||
} else if (col.cast) { | ||
} else if (col.cast && !(options && options.toSql && options.toSql.dialect === 'sqlite')) { | ||
result = env.indent + 'CAST(' + renderExpr(col, env, true) + ' AS '; | ||
@@ -812,2 +818,196 @@ result += renderBuiltinType(col.cast.type) + renderTypeParameters(col.cast); | ||
function prepOnCondition(onCondition){ | ||
return onCondition.map((x,i) => { | ||
if(typeof x === 'string'){ | ||
if(i === 0){ | ||
return `${x} `; | ||
} else if(i === onCondition.length-1){ | ||
return ` ${x}`; | ||
} else { | ||
return ` ${x} `; | ||
} | ||
} else { | ||
return x; | ||
} | ||
}) | ||
} | ||
function renderHdbcdsConformingOnCondition(x, env){ | ||
// Compound expression | ||
if (x instanceof Array) { | ||
// Simply concatenate array parts with spaces (with a tiny bit of beautification) | ||
// FIXME: Take this for `toCdl`, too | ||
let tokens = x.map(item => renderHdbcdsConformingOnCondition(item, env)); | ||
let result = ''; | ||
for (let i = 0; i < tokens.length; i++) { | ||
result += tokens[i]; | ||
// No space after last token, after opening parentheses, before closing parentheses, before comma | ||
} | ||
return result; | ||
} | ||
// Various special cases represented as objects | ||
else if (typeof x === 'object' && x !== null) { | ||
// Literal value, possibly with explicit 'literal' property | ||
if (x.val !== undefined) { | ||
switch (x.literal || typeof x.val) { | ||
case 'number': | ||
case 'boolean': | ||
case 'null': | ||
// 17.42, NULL, TRUE | ||
return String(x.val); | ||
case 'x': | ||
// x'f000' | ||
return `${x.literal}'${x.val}'`; | ||
case 'date': | ||
case 'time': | ||
case 'timestamp': | ||
if (options.toSql.dialect === 'sqlite') { | ||
// date('2017-11-02') | ||
return `${x.literal}('${x.val}')`; | ||
} else { | ||
// date'2017-11-02' | ||
return `${x.literal}'${x.val}'`; | ||
} | ||
case 'string': | ||
// 'foo', with proper escaping | ||
return `'${x.val.replace(/'/g, "''")}'`; | ||
case 'object': | ||
if (x.val === null) { | ||
return 'NULL'; | ||
} | ||
// otherwise fall through to | ||
default: | ||
throw new Error('Unknown literal or type: ' + JSON.stringify(x)); | ||
} | ||
} | ||
// Enum symbol | ||
else if (x['#']) { | ||
// #foo | ||
// FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand) | ||
signal(error`Enum values are not yet supported for conversion to SQL`, x.location); | ||
return ''; | ||
} | ||
// Reference: Array of path steps, possibly preceded by ':' | ||
else if (x.ref) { | ||
if (options.forHana && !x.param && !x.global) { | ||
if(x.ref[0] === '$user') { | ||
// FIXME: this is all not enough: we might need an explicit select item alias | ||
if (x.ref[1] === 'id') { | ||
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) { | ||
return `'${options.toSql.user}'`; | ||
} | ||
else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String)) { | ||
return `'${options.toSql.user.id}'`; | ||
} else { | ||
return "SESSION_CONTEXT ( 'APPLICATIONUSER' ) "; | ||
} | ||
} | ||
} | ||
else if (x.ref[1] === 'locale') { | ||
return "SESSION_CONTEXT ( 'LOCALE' ) "; | ||
} | ||
} | ||
else if(x.ref[0] === '$at') { | ||
if(x.ref[1] === 'from') { | ||
return "SESSION_CONTEXT ( 'VALID-FROM' ) "; | ||
} | ||
else if(x.ref[1] === 'to') { | ||
return "SESSION_CONTEXT ( 'VALID-TO' ) "; | ||
} | ||
} | ||
// FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we | ||
// assume that it was not if the path has length 2 ( | ||
if (firstPathStepId(x.ref) === '$parameters' && x.ref.length == 2) { | ||
// Parameters must be uppercased and unquoted in SQL | ||
return `:${x.ref[1].toUpperCase()}`; | ||
} | ||
if (x.param) { | ||
return `:${x.ref[0].toUpperCase()}`; | ||
} | ||
return x.ref.map(renderPathStep) | ||
.filter(s => s != '') | ||
.map(ps => `"${ps.toUpperCase()}"`) | ||
.join('.'); | ||
} | ||
// Function call, possibly with args (use '=>' for named args) | ||
else if (x.func) { | ||
return renderFunc( x, options.toSql.dialect, a => renderArgs(a, '=>', env), true ); | ||
} | ||
// Nested expression | ||
else if (x.xpr) { | ||
return renderHdbcdsConformingOnCondition(x.xpr, env); | ||
} | ||
// Sub-select | ||
else if (x.SELECT) { | ||
// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView) | ||
return `(${renderQuery('<subselect>', x, increaseIndent(env))})`; | ||
} | ||
else if (x.SET) { | ||
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource) | ||
return `${renderQuery('<union>', x, increaseIndent(env))}`; | ||
} | ||
else { | ||
throw new Error('Unknown expression: ' + JSON.stringify(x)); | ||
} | ||
} | ||
// Not a literal value but part of an operator, function etc - just leave as it is | ||
// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql | ||
else { | ||
return String(x).toUpperCase(); | ||
} | ||
// Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function | ||
function renderPathStep(s, idx) { | ||
// Simple id or absolute name | ||
if (typeof(s) === 'string') { | ||
// TODO: When is this actually executed and not handled already in renderExpr? | ||
const magicForHana = { | ||
'$now': 'CURRENT_TIMESTAMP', | ||
'$user.id': "SESSION_CONTEXT('APPLICATIONUSER')", | ||
'$user.locale': "SESSION_CONTEXT('LOCALE')", | ||
} | ||
// Some magic for first path steps | ||
if (idx == 0) { | ||
// HANA-specific translation of '$now' and '$user' | ||
// FIXME: this is all not enough: we might need an explicit select item alias | ||
if (magicForHana[s]) { | ||
return magicForHana[s]; | ||
} | ||
// Ignore initial $projection and initial $self | ||
if (s === '$projection' || s === '$self') { | ||
return ''; | ||
} | ||
} | ||
return quoteSqlId(s); | ||
} | ||
// ID with filters or parameters | ||
else if (typeof s === 'object') { | ||
// Sanity check | ||
if (!s.func && !s.id) { | ||
throw new Error('Unknown path step object: ' + JSON.stringify(s)); | ||
} | ||
// Not really a path step but an object-like function call | ||
if (s.func) { | ||
return `${s.func}(${renderArgs(s.args, '=>', env)})`; | ||
} | ||
// Path step, possibly with view parameters and/or filters | ||
let result = `"${quoteSqlId(s.id)}"`; | ||
if (s.args) { | ||
// View parameters | ||
result += `(${renderArgs(s.args, '=>', env)})`; | ||
} | ||
if (s.where) { | ||
// Filter, possibly with cardinality | ||
// FIXME: Does SQL understand filter cardinalities? | ||
result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderHdbcdsConformingOnCondition(s.where, env)}]`; | ||
} | ||
return result; | ||
} | ||
else { | ||
throw new Error('Unknown path step: ' + JSON.stringify(s)); | ||
} | ||
} | ||
} | ||
// Render an expression (including paths and values) or condition 'x'. | ||
@@ -814,0 +1014,0 @@ // (no trailing LF, don't indent if inline) |
@@ -381,3 +381,4 @@ 'use strict'; | ||
switch(message.messageId){ | ||
case 'empty-entity-or-type': | ||
case 'empty-entity': | ||
case 'empty-type': | ||
message.severity = 'Error'; | ||
@@ -384,0 +385,0 @@ break; |
@@ -112,3 +112,4 @@ 'use strict'; | ||
isStructured, | ||
inspectRef | ||
inspectRef, | ||
artifactRef | ||
} = getUtils(csn); | ||
@@ -174,4 +175,18 @@ | ||
if (def.kind !== 'entity') { | ||
if (!isStructured(getFinalTypeDef(def))) | ||
toFinalBaseType(def); | ||
let finalTypeDef = def; | ||
if(def.type && def.type.ref) { | ||
finalTypeDef = artifactRef(def.type); | ||
if(!finalTypeDef.type) { | ||
signal(error`"${defName}" has no final type`, ['definitions', defName]); | ||
return; | ||
} | ||
} | ||
if (!isStructured(finalTypeDef)) { | ||
try { | ||
toFinalBaseType(def); | ||
} catch(ex) { | ||
signal(error`"${defName}" final base type not found`, ['definitions', defName]); | ||
return | ||
} | ||
} | ||
toFinalBaseType(def.items); | ||
@@ -279,11 +294,11 @@ toFinalBaseType(def.returns); | ||
if (def.kind === 'action' || def.kind === 'function') { | ||
exposeStructTypesForAction(def, defName, service); | ||
exposeTypesForAction(def, defName, service); | ||
} | ||
for (let actionName in def.actions || {}) { | ||
exposeStructTypesForAction(def.actions[actionName], `${defName}_${actionName}`, service); | ||
exposeTypesForAction(def.actions[actionName], `${defName}_${actionName}`, service); | ||
} | ||
if (def.kind === 'entity' || def.kind === 'view') { | ||
let isAction = false | ||
let isAction = false; | ||
// If a member is of type 'array of T' where T is either user defined structured type outside of the service or anonymous type, | ||
@@ -294,13 +309,9 @@ // then expose T and assign it do the member. | ||
// and on params and returns of action/function | ||
if (member.kind === 'action' || member.kind === 'function') { | ||
isAction = true; | ||
return; | ||
} | ||
if (['params', 'returns'].includes(propertyName) && isAction) return; | ||
if (member.kind === 'action' || member.kind === 'function') isAction = true; | ||
// anonymous defined "array of" | ||
if (member.items || (member.type && getFinalTypeDef(member.type).items)) { | ||
structuredOData ? | ||
exposeArrayOfTypeOf(member, service, `${defNameWithoutServiceName(defName, service)}_${memberName}`, memberName) | ||
: signal(error`"${memberName}": Element must not be an "array of" in flat mode`, path); | ||
if (structuredOData) | ||
exposeArrayOfTypeOf(member, service, `${defNameWithoutServiceName(defName, service)}_${memberName}`, memberName); | ||
else if (!isAction) signal(error`"${memberName}": Element must not be an "array of" in flat mode`, path); | ||
} | ||
@@ -318,6 +329,11 @@ }, ['definitions', defName]); | ||
// This must be done before 4.1, since all composition targets are annotated with @odata.draft.enabled in this step | ||
let sortByAssociationDependency = require('./sortByAssociationDependency'); | ||
let sortedArtifactNames = sortByAssociationDependency(csn); | ||
let flattenedKeyArts = Object.create(null); | ||
forEachDefinition(csn, (def, defName) => { | ||
sortedArtifactNames.forEach(defName => { | ||
let def = csn.definitions[defName]; | ||
flattenForeignKeysForArt(def, defName, flattenedKeyArts); | ||
}); | ||
}) | ||
@@ -433,2 +449,3 @@ // Fourth walk through the model: Now all artificially generated things are in place | ||
let keyCount = 0; | ||
/** @type {[string, CSN.Element][]} */ | ||
let mediaTypes = []; | ||
@@ -518,3 +535,6 @@ // Walk the elements | ||
switch (message.messageId) { | ||
case 'empty-entity-or-type': | ||
case 'enum-value-ref': | ||
case 'empty-entity': | ||
case 'empty-type': | ||
case 'check-proper-type-of': | ||
message.severity = 'Error'; | ||
@@ -634,9 +654,20 @@ break; | ||
// in 'service' and make 'action' use them instead | ||
function exposeStructTypesForAction(action, actionName, service) { | ||
exposeStructTypeOf(action.returns, service, `return_${actionName.replace(/\./g, '_')}`, actionName); | ||
function exposeTypesForAction(action, actionName, service) { | ||
if (action.returns && isArreyed(action.returns)) | ||
exposeArrayOfTypeOf(action.returns, service, `return_${actionName.replace(/\./g, '_')}`, actionName); | ||
else | ||
exposeStructTypeOf(action.returns, service, `return_${actionName.replace(/\./g, '_')}`, actionName); | ||
for (let paramName in action.params || {}) { | ||
exposeStructTypeOf(action.params[paramName], service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, actionName); | ||
if (isArreyed(action.params[paramName])) | ||
exposeArrayOfTypeOf(action.params[paramName], service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, actionName); | ||
else | ||
exposeStructTypeOf(action.params[paramName], service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, actionName); | ||
} | ||
} | ||
function isArreyed(node) { | ||
return node.items || (node.type && getFinalTypeDef(node.type).items); | ||
} | ||
// If a member is of type "array of <named type|anonymous type>", we expose the arrayed type, | ||
@@ -651,17 +682,23 @@ // like we expose structures in structured mode | ||
} | ||
// handle the case: 'elem: array of Foo' and 'type Foo: array of ...' | ||
// we do not see the 'items' property in the element and | ||
// need to create a new type, which holds the items property | ||
else if (node.type && getFinalTypeDef(node.type).items) { | ||
if (!isArtifactInService(node.type, service)) { | ||
let typeId = `${service}.${node.type}`; | ||
let newType = exposeArrayedType(getFinalTypeDef(node.type), typeId); | ||
// When we have in the model something like: | ||
// type Foo: array of Bar; type Bar: { qux: Integer }; | ||
// In the type Foo we expand the first level of elements of the items like we have in CDL this: | ||
// type Foo: array of { qux: Integer }; | ||
expandFirstLevelOfArrayed(newType); | ||
node.type = typeId; | ||
// we can have both of the 'type' and 'items' in the cases: | ||
// 1. 'elem: Foo' and 'type Foo: array of Baz' and 'type Baz: { ... }' | ||
// or 2. 'elem: Foo' and type Foo: array of Integer|String|...' | ||
else if (node.type) { | ||
// case 2. - in V2 we expand to the underlying base scalar and remove the type property | ||
if (node.items && node.items.type && isBuiltinType(node.items.type) | ||
&& options.toOdata.version === 'v2') delete node.type; | ||
else if (getFinalTypeDef(node.type).items) { | ||
if (!isArtifactInService(node.type, service)) { | ||
let typeId = `${service}.${node.type}`; | ||
let newType = exposeArrayedType(getFinalTypeDef(node.type), typeId); | ||
// When we have in the model something like: | ||
// type Foo: array of Bar; type Bar: { qux: Integer }; | ||
// In the type Foo we expand the first level of elements of the items like we have in CDL this: | ||
// type Foo: array of { qux: Integer }; | ||
expandFirstLevelOfArrayed(newType); | ||
node.type = typeId; | ||
} | ||
// case 1. - as we keep the type property, the items property is removed | ||
if (node.items) delete node.items; | ||
} | ||
if (node.items) delete node.items; | ||
} | ||
@@ -718,3 +755,3 @@ | ||
} | ||
copyAnnotations(node, type); | ||
typeDef.kind === 'type' ? copyAnnotations(typeDef, type) : copyAnnotations(node, type); | ||
if (structuredOData) delete node.elements; | ||
@@ -765,6 +802,8 @@ node.type = `${service}.${typeId}`; | ||
flattenedKeyArts[defName] = true; | ||
let rootPath = ['definitions', defName] | ||
const rootPath = ['definitions', defName]; | ||
forEachMemberRecursively(def, (member, memberName, prop, subpath, parent) => { | ||
// Generate foreign key elements for managed associations | ||
if (isManagedAssociationElement(member)) { | ||
// Generate foreign key elements for managed associations. | ||
// In the case of composition of aspect we might have an element which does not | ||
// have on-cond and keys, therefore the check if a member has a 'keys' | ||
if (isManagedAssociationElement(member) && member.keys) { | ||
// Flatten foreign keys (replacing foreign keys that are managed associations by their respective foreign keys) | ||
@@ -818,3 +857,3 @@ flattenForeignKeys(member, flattenedKeyArts, flattenForeignKeysForArt); | ||
if (!structuredOData) // flat-mode | ||
flattenOnCond(member, memberName, def.elements, defName); | ||
flattenOnCond(member, memberName, def.elements, defName, rootPath.concat(subpath)); | ||
else // structured-mode | ||
@@ -885,3 +924,2 @@ normalizeOnCondForStructuredMode(member) | ||
} | ||
// Generate the annotations describing the draft actions (only draft roots can be activated/edited) | ||
@@ -888,0 +926,0 @@ if (artifact == rootArtifact) { |
@@ -7,3 +7,3 @@ 'use strict'; | ||
const { setProp, cloneWithTransformations } = require('../base/model'); | ||
const { addStringAnnotationTo, printableName, | ||
const { addBoolAnnotationTo, addStringAnnotationTo, printableName, | ||
copyAnnotations, isStructuredElement, hasBoolAnnotation } = require('../model/modelUtils'); | ||
@@ -624,2 +624,4 @@ const alerts = require('../base/alerts'); | ||
let draftUuid = createScalarElement('DraftUUID', 'cds.UUID', true, undefined); | ||
addBoolAnnotationTo('@UI.Hidden', true, draftUuid); | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_DraftUUID}', draftUuid); | ||
addElement(draftUuid, artifact); | ||
@@ -629,2 +631,3 @@ | ||
let creationDateTime = createScalarElement('CreationDateTime', 'cds.Timestamp', false, undefined); | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_CreationDateTime}', creationDateTime); | ||
addElement(creationDateTime, artifact); | ||
@@ -635,2 +638,3 @@ | ||
createdByUser.length = { literal: 'number', val: 256 }; | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_CreatedByUser}', createdByUser); | ||
addElement(createdByUser, artifact); | ||
@@ -640,2 +644,4 @@ | ||
let draftIsCreatedByMe = createScalarElement('DraftIsCreatedByMe', 'cds.Boolean', false, undefined); | ||
addBoolAnnotationTo('@UI.Hidden', true, draftIsCreatedByMe); | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_DraftIsCreatedByMe}', draftIsCreatedByMe); | ||
addElement(draftIsCreatedByMe, artifact); | ||
@@ -645,2 +651,3 @@ | ||
let lastChangeDateTime = createScalarElement('LastChangeDateTime', 'cds.Timestamp', false, undefined); | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_LastChangeDateTime}', lastChangeDateTime); | ||
addElement(lastChangeDateTime, artifact); | ||
@@ -651,2 +658,3 @@ | ||
lastChangedByUser.length = { literal: 'number', val: 256 }; | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_LastChangedByUser}', lastChangedByUser); | ||
addElement(lastChangedByUser, artifact); | ||
@@ -657,2 +665,3 @@ | ||
inProcessByUser.length = { literal: 'number', val: 256 }; | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_InProcessByUser}', inProcessByUser); | ||
addElement(inProcessByUser, artifact); | ||
@@ -662,2 +671,4 @@ | ||
let draftIsProcessedByMe = createScalarElement('DraftIsProcessedByMe', 'cds.Boolean', false, undefined); | ||
addBoolAnnotationTo('@UI.Hidden', true, draftIsProcessedByMe); | ||
addStringAnnotationTo('@Common.Label', '{i18n>Draft_DraftIsProcessedByMe}', draftIsProcessedByMe); | ||
addElement(draftIsProcessedByMe, artifact); | ||
@@ -664,0 +675,0 @@ |
@@ -230,2 +230,3 @@ 'use strict'; | ||
foreignKey.$generatedFieldName = foreignKeyElementName; | ||
setProp(foreignKey, "$path", path); // attach $path to the newly created element - used for inspectRef in processAssociationOrComposition | ||
@@ -311,2 +312,10 @@ result[foreignKeyElementName] = foreignKeyElement; | ||
let result = Object.create(null); | ||
const addGeneratedFlattenedElement = (e, eName) => { | ||
if(result[eName]){ | ||
signal(error`Generated element ${eName} conflicts with other generated element`, pathInCsn) | ||
} else { | ||
result[eName] = e; | ||
} | ||
} | ||
for (let childName in struct) { | ||
@@ -320,3 +329,3 @@ let childElem = struct[childName]; | ||
let flatElem = grandChildElems[grandChildName]; | ||
result[flatElemName] = flatElem; | ||
addGeneratedFlattenedElement(flatElem, flatElemName); | ||
// TODO: check with values. In CSN such elements have only "@Core.Computed": true | ||
@@ -328,3 +337,2 @@ // If the original element had a value, construct one for the flattened element | ||
// Preserve the generated element name as it would have been with 'hdbcds' names | ||
result[flatElemName] = flatElem; | ||
} | ||
@@ -337,3 +345,3 @@ } else { | ||
setProp(flatElem, '_flatElementNameWithDots', elementPath.concat(childName).join('.')); | ||
result[flatElemName] = flatElem; | ||
addGeneratedFlattenedElement(flatElem, flatElemName); | ||
} | ||
@@ -406,3 +414,3 @@ } | ||
// the non-enumerable property '_flatElementNameWithDots'. | ||
function flattenOnCond(assoc, assocName, defElements, defName) { | ||
function flattenOnCond(assoc, assocName, defElements, defName, path) { | ||
@@ -452,3 +460,3 @@ if (!assoc.on) return; // nothing to do | ||
node.ref.splice(0, 1); | ||
}, ['definitions', defName, 'elements', assocName]); | ||
}, path); | ||
} | ||
@@ -506,2 +514,4 @@ | ||
let finalBaseType = getFinalBaseType(node.type); | ||
if(finalBaseType === null) | ||
throw Error('Failed to obtain final base type for reference : ' + node.type.ref.join('/')); | ||
if (finalBaseType.elements) { | ||
@@ -619,2 +629,4 @@ node.elements = finalBaseType.elements; // copy elements | ||
let draftUuid = createScalarElement('DraftUUID', 'cds.UUID', true); | ||
draftUuid.DraftUUID['@UI.Hidden'] = true; | ||
draftUuid.DraftUUID['@Common.Label'] = '{i18n>Draft_DraftUUID}'; | ||
addElement(draftUuid, artifact, artifactName); | ||
@@ -624,2 +636,3 @@ | ||
let creationDateTime = createScalarElement('CreationDateTime', 'cds.Timestamp'); | ||
creationDateTime.CreationDateTime['@Common.Label'] = '{i18n>Draft_CreationDateTime}'; | ||
addElement(creationDateTime, artifact, artifactName); | ||
@@ -630,2 +643,3 @@ | ||
createdByUser['CreatedByUser'].length = 256; | ||
createdByUser.CreatedByUser['@Common.Label'] = '{i18n>Draft_CreatedByUser}'; | ||
addElement(createdByUser, artifact, artifactName); | ||
@@ -635,2 +649,4 @@ | ||
let draftIsCreatedByMe = createScalarElement('DraftIsCreatedByMe', 'cds.Boolean'); | ||
draftIsCreatedByMe.DraftIsCreatedByMe['@UI.Hidden'] = true; | ||
draftIsCreatedByMe.DraftIsCreatedByMe['@Common.Label'] = '{i18n>Draft_DraftIsCreatedByMe}'; | ||
addElement(draftIsCreatedByMe, artifact, artifactName); | ||
@@ -640,2 +656,3 @@ | ||
let lastChangeDateTime = createScalarElement('LastChangeDateTime', 'cds.Timestamp'); | ||
lastChangeDateTime.LastChangeDateTime['@Common.Label'] = '{i18n>Draft_LastChangeDateTime}'; | ||
addElement(lastChangeDateTime, artifact, artifactName); | ||
@@ -646,2 +663,3 @@ | ||
lastChangedByUser['LastChangedByUser'].length = 256; | ||
lastChangedByUser.LastChangedByUser['@Common.Label'] = '{i18n>Draft_LastChangedByUser}'; | ||
addElement(lastChangedByUser, artifact, artifactName); | ||
@@ -652,2 +670,3 @@ | ||
inProcessByUser['InProcessByUser'].length = 256; | ||
inProcessByUser.InProcessByUser['@Common.Label'] = '{i18n>Draft_InProcessByUser}'; | ||
addElement(inProcessByUser, artifact, artifactName); | ||
@@ -657,2 +676,4 @@ | ||
let draftIsProcessedByMe = createScalarElement('DraftIsProcessedByMe', 'cds.Boolean'); | ||
draftIsProcessedByMe.DraftIsProcessedByMe['@UI.Hidden'] = true; | ||
draftIsProcessedByMe.DraftIsProcessedByMe['@Common.Label'] = '{i18n>Draft_DraftIsProcessedByMe}'; | ||
addElement(draftIsProcessedByMe, artifact, artifactName); | ||
@@ -783,3 +804,3 @@ | ||
* @param {any} elem is in form: { b: { type: 'cds.String' } } | ||
* @param {any} artifact is: { kind: 'entity', elements: { a: { type: 'cds.Integer' } ... } } | ||
* @param {CSN.Artifact} artifact is: { kind: 'entity', elements: { a: { type: 'cds.Integer' } ... } } | ||
* @param {string} [artifactName] Name of the artifact in `csn.definitions[]`. | ||
@@ -816,3 +837,3 @@ * @returns {void} | ||
* @param {object} elem | ||
* @param {object} artifact | ||
* @param {CSN.Artifact} artifact | ||
* @param {string} artifactName | ||
@@ -875,3 +896,3 @@ * @param {string} elementName | ||
* In form of `{ myAction: { kind: 'action', returns ... } }` | ||
* @param {object} artifact Artifact in the form of `{ kind: 'entity', elements: ... }` | ||
* @param {CSN.Artifact} artifact Artifact in the form of `{ kind: 'entity', elements: ... }` | ||
* @param {string} artifactName Name of the artifact (used for error locations). | ||
@@ -928,7 +949,7 @@ **/ | ||
* | ||
* @param {any} annoName Annotation name | ||
* @param {any} element Element to be checked | ||
* @param {any[]} path | ||
* @param {any} artifact Artifact | ||
* @returns {Boolean} True if no errors | ||
* @param {string} annoName Annotation name | ||
* @param {object} element Element to be checked | ||
* @param {string[]} path | ||
* @param {CSN.Artifact} artifact | ||
* @returns {boolean} True if no errors | ||
*/ | ||
@@ -951,3 +972,3 @@ function checkAssignment(annoName, element, path, artifact) { | ||
* @param {any} annoName Name of the annotation | ||
* @param {any} artifact Root artifact containing the elements | ||
* @param {CSN.Artifact} artifact Root artifact containing the elements | ||
* @param {string} artifactName Name of the root artifact | ||
@@ -969,5 +990,5 @@ * @param {boolean} [err=true] Down-grade to a warning if set to false | ||
* | ||
* @param artifact the artifact | ||
* @param path path to get to `artifact` (mainly used for error messages) | ||
* @param callback the callback to be called | ||
* @param {CSN.Artifact} artifact the artifact | ||
* @param {string[]} path path to get to `artifact` (mainly used for error messages) | ||
* @param {(art: CSN.Artifact, path: string[]) => any} callback Function called for each element recursively. | ||
*/ | ||
@@ -974,0 +995,0 @@ function recurseElements(artifact, path, callback) { |
@@ -1,1 +0,1 @@ | ||
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"^1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.24.4","license":"SEE LICENSE IN developer-license-3.1.txt"} | ||
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"^1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.26.2","license":"SEE LICENSE IN developer-license-3.1.txt"} |
@@ -8,3 +8,5 @@ # Getting started | ||
[Installation and Usage](#installation-and-usage) | ||
[Command invocation](#command-invocation) | ||
[Command invocation](#command-invocation) | ||
[Build from source](#build-from-source) | ||
[Documentation](#documentation) | ||
@@ -41,1 +43,22 @@ ## Installation and Usage | ||
* `2`: commmand invocation error (invalid options, repeated file name) | ||
### Build from source | ||
We recommend to install cds-compiler using npm. However, if you want to use | ||
the latest master (e.g. for testing purposes) then you need to set up the | ||
compiler first: | ||
```sh | ||
git clone git@github.wdf.sap.corp:cdx/cds-compiler.git | ||
cd cds-compiler | ||
npm install | ||
npm run download # Downloads Antlr (Java Dependency) | ||
npm run gen # Generates the parser | ||
./bin/cdsc.js --help | ||
``` | ||
## Documentation | ||
Please refer to the [official CDS documentation][capire]. | ||
[capire]: https://cap.cloud.sap/docs/cds/ |
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 too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
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
3488198
104
66378
63