UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

501 lines (466 loc) 15.8 kB
// The builtin artifacts of CDS // TODO: split this file // - in base/: common definitions, datetime formats // - in compiler/: XSN-specific // - in ?: CSN-specific 'use strict'; const { builtinLocation } = require('../base/location'); const { setLink: setProp } = require('./utils'); const { isBetaEnabled } = require('../base/model'); // TODO: make type parameters a dict const core = { String: { parameters: [ 'length' ], category: 'string' }, LargeString: { category: 'string' }, Binary: { parameters: [ 'length' ], category: 'binary' }, LargeBinary: { category: 'binary' }, Decimal: { parameters: [ 'precision', 'scale' ], category: 'decimal' }, DecimalFloat: { category: 'decimal', deprecated: true }, Integer64: { category: 'integer' }, Integer: { category: 'integer' }, UInt8: { category: 'integer' }, Int16: { category: 'integer' }, Int32: { category: 'integer' }, Int64: { category: 'integer' }, Double: { category: 'decimal' }, Date: { category: 'dateTime' }, Time: { category: 'dateTime' }, DateTime: { category: 'dateTime' }, Timestamp: { category: 'dateTime' }, Boolean: { category: 'boolean' }, UUID: { category: 'string' }, Vector: { parameters: [ 'length' /* , 'type' */ ], category: 'vector' }, Map: { category: 'map' }, Association: { internal: true, category: 'relation' }, Composition: { internal: true, category: 'relation' }, }; const coreHana = { // ALPHANUM: { parameters: [ 'length' ] }, SMALLINT: { category: 'integer' }, TINYINT: { category: 'integer' }, SMALLDECIMAL: { category: 'decimal' }, REAL: { category: 'decimal' }, CHAR: { parameters: [ 'length' ], category: 'string' }, NCHAR: { parameters: [ 'length' ], category: 'string' }, VARCHAR: { parameters: [ 'length' ], category: 'string' }, CLOB: { category: 'string' }, BINARY: { parameters: [ 'length' ], category: 'binary' }, // TODO: probably remove default for ST_POINT, ST_GEOMETRY (to be done in backend); ST_POINT: { parameters: [ { name: 'srid', literal: 'number', val: 0 } ], category: 'geo' }, ST_GEOMETRY: { parameters: [ { name: 'srid', literal: 'number', val: 0 } ], category: 'geo' }, }; const typeParameters = { expectedLiteralsFor: { length: [ 'number' ], scale: [ 'number', 'string' ], precision: [ 'number' ], srid: [ 'number' ], }, }; // a.k.a "typeProperties" typeParameters.list = Object.keys( typeParameters.expectedLiteralsFor ); // const hana = { // BinaryFloat: {}, // LocalDate: {}, // LocalTime: {}, // UTCDateTime: {}, // UTCTimestamp: {}, // WithStructuredPrivilegeCheck: { kind: 'annotation' }, // hana: { kind: 'context' }, // }; const specialFunctions = compileFunctions( { // TODO: use lower-case '': [ // the default { intro: [ 'ALL', 'DISTINCT' ], introMsg: [], // do not list them in code completion }, {}, ], ROUND: [ null, null, { // 3rd argument: rounding mode expr: [ 'ROUND_HALF_UP', 'ROUND_HALF_DOWN', 'ROUND_HALF_EVEN', 'ROUND_UP', 'ROUND_DOWN', 'ROUND_CEILING', 'ROUND_FLOOR' ], }, ], TRIM: [ { intro: [ 'LEADING', 'TRAILING', 'BOTH' ], expr: [ 'LEADING', 'TRAILING', 'BOTH' ], separator: [ 'FROM' ], }, ], EXTRACT: [ { expr: [ 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND' ], separator: [ 'FROM' ], }, ], COUNT: [ { expr: [ '*' ], intro: [ 'ALL', 'DISTINCT' ], }, ], MIN: 'COUNT', MAX: 'COUNT', SUM: 'COUNT', AVG: 'COUNT', STDDEV: 'COUNT', VAR: 'COUNT', LOCATE_REGEXPR: [ { intro: [ 'START', 'AFTER' ], separator: [ 'FLAG', 'IN', 'FROM', 'OCCURRENCE', 'GROUP' ], }, ], OCCURRENCES_REGEXPR: [ { separator: [ 'FLAG', 'IN', 'FROM' ], }, ], REPLACE_REGEXPR: [ { separator: [ 'FLAG', 'IN', 'WITH', 'FROM', 'OCCURRENCE' ], expr: [ 'ALL' ], }, ], SUBSTRING_REGEXPR: [ { separator: [ 'FLAG', 'IN', 'FROM', 'OCCURRENCE', 'GROUP' ], }, ], SUBSTR_REGEXPR: 'SUBSTRING_REGEXPR', } ); function compileFunctions( special ) { const compiled = {}; for (const [ name, val ] of Object.entries( special )) compiled[name] = (typeof val === 'string' ? special[val] : val).map( compileArg ); return compiled; } function compileArg( src ) { if (!src) return src; const tgt = { intro: src.intro || [], expr: src.expr || [], separator: src.separator || [], }; for (const generic of [ 'intro', 'expr', 'separator' ]) { // intro before expr: if both intro and expr, tag as 'expr' for (const token of src[generic] || []) tgt[token] = generic; } // As GenericIntro is always together with GenericExpr, only mention those // which are not already proposed for GenericExpr: tgt.introMsg = src.introMsg || tgt.intro.filter( token => tgt[token] === 'intro' ); return tgt; } /** * Variables that have special meaning in CDL/CSN. */ const magicVariables = { $user: { // always available elements: { id: {}, locale: {} }, // Allow $user.<any> $uncheckedElements: true, // Allow shortcut in CDL: `$user` becomes `$user.id` in CSN. $autoElement: 'id', }, $at: { // $at is considered deprecated since cds-compiler v5 elements: { from: {}, to: {}, }, // Require that elements are accessed, i.e. no $at, only $at.<element>. $requireElementAccess: true, deprecated: true, // $at is deprecated; use $valid }, $valid: { elements: { from: {}, to: {}, }, // Require that elements are accessed, i.e. no $valid, only $valid.<element>. $requireElementAccess: true, }, $now: {}, $tenant: { $requiresBetaFlag: 'tenantVariable' }, $session: { // In ABAP CDS session variables are accessed in a generic way via // the pseudo variable $session. $uncheckedElements: true, $requireElementAccess: true, }, $draft: { elements: { IsActiveEntity: {}, HasActiveEntity: {}, HasDraftEntity: {}, }, // Require that elements are accessed, i.e. no $draft, only $draft.<element>. $requireElementAccess: true, $restricted: true, // only in annotation expression, see shared.js }, }; // see lib/render/renderUtil.js for DB-specific magic vars, specified in CAP CDS via function const dateRegEx = /^(-?\d{4})-(\d{1,2})-(\d{1,2})$/; // YYYY - MM - dd const timeRegEx = /^T?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:Z|[+-]\d{2}(?::\d{2})?)?$/; // T HH : mm : ss TZD // eslint-disable-next-line @stylistic/max-len, sonarjs/regex-complexity const timestampRegEx = /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?(?:Z|[+-]\d{2}(?::\d{2})?)?$/; // YYYY - MM - dd T HH : mm : ss . fraction TZD // eslint-disable-next-line sonarjs/regex-complexity const numberRegEx = /^[ \t]*[-+]?(\d+(\.\d*)?|\.\d+)(e[-+]?\d+)?[ \t]*$/i; /** * Patterns for literal token tests and creation. The value is a map from the * `prefix` argument of function `quotedliteral` to the following properties: * - `test_msg`: error message which is issued if `test_fn` fails. * - `test_fn`: function called with argument `value`, fails falsy return value * - `unexpected_msg`: error message which is issued if `unexpected_char` matches * - `unexpected_char`: regular expression matching an illegal character in `value`, * the error location is only correct for a literal <prefix>'<value>' * TODO: we might do a range check (consider leap seconds, i.e. max value 60), * but always allow Feb 29 (no leap year computation) * Notes: * - Dates/Times as defined in ISO 8601, see <https://en.wikipedia.org/wiki/ISO_8601> */ const quotedLiteralPatterns = { x: { test_variant: 'uneven-hex', test_fn: (str => Number.isInteger( str.length / 2 )), unexpected_variant: 'invalid-hex', unexpected_char: /[^0-9a-f]/i, json_type: 'string', }, time: { test_variant: 'time', test_fn: (x) => { // Leading `T` allowed in ISO 8601. const match = x.match( timeRegEx ); return match !== null && checkTime( match[1], match[2], match[3] ); }, json_type: 'string', }, date: { test_variant: 'date', test_fn: (x) => { const match = x.match( dateRegEx ); return match !== null && checkDate( match[1], match[2], match[3] ); }, json_type: 'string', }, timestamp: { test_variant: 'timestamp', test_fn: (x) => { const match = x.match( timestampRegEx ); return match !== null && checkDate( match[1], match[2], match[3] ) && checkTime( match[4], match[5], match[6] ); }, json_type: 'string', }, // and only for CSN parser: null: { json_type: 'null', // modulo JSON typeof weirdness }, boolean: { json_type: 'boolean', }, number: { test_variant: 'number', test_fn: (x => numberRegEx.test( x )), json_type: 'number', secondary_json_type: 'string', }, string: { json_type: 'string', }, }; /** * Check that the given date is within boundaries. * We can't use Date.parse() since that also allows non-standard values (2022-02-31 for example). * Checks according to ISO 8601. * * @returns {boolean} True if the date is valid. */ function checkDate( year, month, day ) { // Negative years are allowed year = Math.abs( Number.parseInt( year, 10 ) ); month = Number.parseInt( month, 10 ); day = Number.parseInt( day, 10 ); // If any is NaN, the condition will be false. // Year 0 does not exist, but ISO 8601 allows it and defines it as 1 BC. return !Number.isNaN( year ) && month > 0 && month < 13 && day > 0 && day < 32; } /** * Check that the given time is within boundaries. * Checks according to ISO 8601. * * @returns {boolean} True if the date is valid. */ function checkTime( hour, minutes, seconds ) { hour = Number.parseInt( hour, 10 ); minutes = Number.parseInt( minutes, 10 ); seconds = seconds ? Number.parseInt( seconds, 10 ) : 0; if (hour === 24) // allow 24:00:00 (ISO 8601 version earlier than 2019) return minutes === 0 && seconds === 0; // If any is NaN, the condition will be false. return hour >= 0 && hour < 24 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 61; // we allow 60 for lead seconds } /** All types belong to one category. */ const typeCategories = { string: [], integer: [], dateTime: [], time: [], decimal: [], binary: [], boolean: [], relation: [], geo: [], vector: [], map: [], }; // Fill type categories with `cds.*` types Object.keys( core ).forEach( (type) => { if (core[type].category) typeCategories[core[type].category].push( `cds.${ type }` ); } ); // Fill type categories with `cds.hana.*` types Object.keys( coreHana ).forEach( (type) => { if (coreHana[type].category) typeCategories[coreHana[type].category].push( `cds.hana.${ type }` ); } ); /** @param {string} typeName */ function isGeoTypeName( typeName ) { return typeCategories.geo.includes( typeName ); } /** * Add CDS builtins like the `cds` namespace with types like `cds.Integer` to * `definitions` of the XSN model as well as to `$builtins`. * * @param {XSN.Model} model XSN model without CDS builtins */ function initBuiltins( model ) { const { options } = model; setMagicVariables( magicVariables ); // namespace:"cds" stores the builtins --- const cds = createNamespace( 'cds', 'reserved' ); model.definitions.cds = cds; // Also add the core artifacts to model.definitions` const c = { ...core }; model.$builtins = env( c, 'cds.', cds ); model.$builtins.cds = cds; // namespace:"cds.hana" stores HANA-specific builtins --- const hana = createNamespace( 'cds.hana', 'reserved' ); model.definitions['cds.hana'] = hana; model.$builtins.hana = hana; cds._subArtifacts.hana = hana; env( coreHana, 'cds.hana.', hana ); model.$internal = { $frontend: '$internal' }; // namespace:"localized" reserved --- model.definitions.localized = createNamespace( 'localized', 'reserved' ); return; function createNamespace( name, builtin ) { const art = { kind: 'namespace', // builtin namespaces don't have a cds file, so no location available name: { id: name, location: builtinLocation() }, blocks: [], builtin, location: builtinLocation(), }; setProp( art, '_subArtifacts', Object.create( null ) ); return art; } /** * Insert the builtins into the parent's `_subArtifacts` dictionary without the * prefix and into the model's `definitions` dictionary prefixed. * * @param {object} builtins Object containing the builtin types. * @param {string} prefix Type prefix, e.g. 'cds.' * @param {object} parent * @returns {object} Artifacts dictionary with the builtin artifacts without prefixes. */ function env( builtins, prefix, parent ) { const artifacts = Object.create( null ); for (const name of Object.keys( builtins )) { const id = prefix + name; // TODO: reconsider whether to set a type to itself - looks wrong const art = { kind: 'type', builtin: true, name: { id }, }; if (parent) parent._subArtifacts[name] = art; setProp( art, '_origin', '' ); setProp( art, '_effectiveType', art ); setProp( art, '_deps', [] ); Object.assign( art, builtins[name] ); if (!art.internal) artifacts[name] = art; model.definitions[id] = art; } return artifacts; } function setMagicVariables( builtins ) { const elements = Object.create( null ); model.$magicVariables = { kind: '$magicVariables', elements }; for (const id in builtins) { const magic = builtins[id]; if (magic.$requiresBetaFlag && !isBetaEnabled( options, magic.$requiresBetaFlag )) continue; // TODO: rename to $builtinFunction const art = { kind: 'builtin', // TODO: $var name: { id }, }; elements[id] = art; setProp( art, '_parent', model.$magicVariables ); if (magic.$autoElement) art.$autoElement = magic.$autoElement; if (magic.$uncheckedElements) art.$uncheckedElements = magic.$uncheckedElements; if (magic.$requireElementAccess) art.$requireElementAccess = magic.$requireElementAccess; if (magic.deprecated) art.deprecated = magic.deprecated; if (magic.$restricted) art.$restricted = magic.$restricted; createMagicElements( art, magic.elements ); if (options.variableReplacements?.[id]) createMagicElements( art, options.variableReplacements[id] ); // setProp( art, '_effectiveType', art ); } } function createMagicElements( art, elements ) { if (!elements) return; const names = Object.keys( elements ); if (names.length > 0 && !art.elements) art.elements = Object.create( null ); for (const id of names) { const magic = { kind: 'builtin', // TODO: '$var' name: { id }, }; // Propagate this property so that it is available for sub-elements. if (art.$uncheckedElements) magic.$uncheckedElements = art.$uncheckedElements; if (art.$restricted) magic.$restricted = art.$restricted; setProp( magic, '_parent', art ); // setProp( magic, '_effectiveType', magic ); if (elements[id] && typeof elements[id] === 'object') createMagicElements( magic, elements[id] ); art.elements[id] = magic; } } } module.exports = { typeParameters, specialFunctions, quotedLiteralPatterns, initBuiltins, isGeoTypeName, };