@sap/cds 8.5.1 → 8.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +12 -9
  5. package/lib/compile/for/java.js +15 -3
  6. package/lib/compile/for/lean_drafts.js +44 -34
  7. package/lib/compile/for/nodejs.js +19 -10
  8. package/lib/compile/minify.js +2 -4
  9. package/lib/compile/parse.js +106 -72
  10. package/lib/compile/to/edm.js +19 -9
  11. package/lib/compile/to/hana.js +25 -21
  12. package/lib/compile/to/json.js +2 -2
  13. package/lib/compile/to/sql.js +15 -8
  14. package/lib/core/linked-csn.js +10 -4
  15. package/lib/dbs/cds-deploy.js +1 -1
  16. package/lib/env/cds-env.js +76 -66
  17. package/lib/env/defaults.js +1 -0
  18. package/lib/i18n/bundles.js +2 -1
  19. package/lib/i18n/files.js +3 -3
  20. package/lib/i18n/localize.js +2 -2
  21. package/lib/index.js +24 -18
  22. package/lib/ql/CREATE.js +11 -6
  23. package/lib/ql/DELETE.js +12 -9
  24. package/lib/ql/DROP.js +15 -8
  25. package/lib/ql/INSERT.js +19 -14
  26. package/lib/ql/SELECT.js +95 -168
  27. package/lib/ql/UPDATE.js +23 -14
  28. package/lib/ql/UPSERT.js +15 -2
  29. package/lib/ql/Whereable.js +44 -118
  30. package/lib/ql/cds-ql.js +222 -28
  31. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  32. package/lib/ql/cds.ql-predicates.js +133 -0
  33. package/lib/ql/cds.ql-projections.js +111 -0
  34. package/lib/ql/cqn.d.ts +146 -0
  35. package/lib/srv/cds-connect.js +3 -3
  36. package/lib/srv/cds-serve.js +2 -2
  37. package/lib/srv/cds.Service.js +132 -0
  38. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  39. package/lib/srv/cds.ServiceProvider.js +20 -0
  40. package/lib/srv/factory.js +20 -8
  41. package/lib/srv/protocols/hcql.js +2 -3
  42. package/lib/srv/protocols/index.js +3 -3
  43. package/lib/srv/srv-dispatch.js +7 -6
  44. package/lib/srv/srv-handlers.js +103 -113
  45. package/lib/srv/srv-methods.js +14 -14
  46. package/lib/srv/srv-tx.js +5 -3
  47. package/lib/utils/cds-utils.js +2 -2
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  49. package/libx/_runtime/cds.js +2 -1
  50. package/libx/_runtime/common/aspects/service.js +25 -0
  51. package/libx/_runtime/common/generic/auth/index.js +5 -0
  52. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  53. package/libx/_runtime/common/generic/auth/service.js +24 -0
  54. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  55. package/libx/_runtime/common/generic/etag.js +1 -1
  56. package/libx/_runtime/common/utils/cqn.js +1 -2
  57. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  58. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  59. package/libx/_runtime/common/utils/postProcess.js +4 -1
  60. package/libx/_runtime/common/utils/restrictions.js +1 -0
  61. package/libx/_runtime/fiori/lean-draft.js +54 -43
  62. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  63. package/libx/_runtime/remote/Service.js +2 -0
  64. package/libx/_runtime/remote/utils/client.js +12 -0
  65. package/libx/odata/index.js +5 -3
  66. package/libx/odata/middleware/create.js +2 -2
  67. package/libx/odata/middleware/delete.js +2 -2
  68. package/libx/odata/middleware/operation.js +2 -2
  69. package/libx/odata/middleware/read.js +14 -12
  70. package/libx/odata/middleware/service-document.js +16 -8
  71. package/libx/odata/middleware/update.js +2 -2
  72. package/libx/odata/parse/afterburner.js +63 -29
  73. package/libx/odata/parse/grammar.peggy +95 -0
  74. package/libx/odata/parse/parser.js +1 -1
  75. package/libx/odata/utils/index.js +5 -1
  76. package/libx/odata/utils/metadata.js +69 -75
  77. package/libx/odata/utils/postProcess.js +24 -3
  78. package/package.json +1 -1
  79. package/server.js +1 -1
  80. package/lib/ql/parse.js +0 -36
  81. /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,8 +1,61 @@
1
1
  # Change Log
2
2
 
3
3
  - All notable changes to this project are documented in this file.
4
- - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
- - This project adheres to [Semantic Versioning](http://semver.org/).
4
+ - The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
+ - This project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## Version 8.6.1 - 2025-01-10
8
+
9
+ ### Fixed
10
+
11
+ - `default-env.json` was not loaded anymore when in production mode.
12
+ - i18n texts like `1` or `true` were returned as numbers, or booleans instead of strings
13
+ - CSN files produced by `cds build` now again contain information to resolve handler files. That was broken in case of reflected/linked models set by e.g. plugins.
14
+ - `average` aggregation used with draft enabled entities
15
+
16
+ ## Version 8.6.0 - 2024-12-17
17
+
18
+ ### Added
19
+
20
+ - `SELECT.from` now supports full-query tagged template literals, e.g.: ``` SELECT.from`Books where ID=${201}` ```
21
+ - `cds.ql` enhanced by functions to facilitate construction of CQN objects.
22
+ - `cds.ql` became a function to turn CQN objects, CQL strings, or tagged template literals into instances of the respective `cds.ql` class.
23
+ - New `cds` events to allow multitenant plugins: `compile.for.runtime`, `compile.to.dbx`, `compile.to.edmx`.
24
+ - `cds.env` now supports `.cdsrc.js` and `.cdsrc.yaml` files, also in plugins.
25
+ - `cds.env` now supports profile-specific `.env` files, e.g. `.hybrid.env` or `.attic.env`.
26
+ - Experimental OData parsing for hierarchy requests (`descendants`, `ancestors`, `TopLevels`)
27
+ - The new OData adapter now supports `cds.odata.containment`. Contained entities can only be accessed via their parents and do not show up as EntitySets in $metadata and the service document.
28
+
29
+ ### Changed
30
+
31
+ - `CDL`, `CQL`, and `CXL` globals are deprecated => use respective functions from `cds.ql` instead.
32
+ - `CREATE`, and `DROP` globals are deprecated => use respective functions from `cds.ql` instead.
33
+ - Zulu time zone information is stripped from `cds.DateTime` properties when querying Odata V2 remote services
34
+ - Processing of `@restrict.where` was aligned with CAP Java:
35
+ + Instance-based authorization on app service calls does not consider custom `WHERE` clauses of `UPDATE`/`DELETE` queries
36
+ + Until `@sap/cds^9`, this change can be deactivated via `cds.env.features.compat_restrict_where = true`
37
+ + Simple static clauses (e.g., `$user.level > 5`) are no longer evaluated by the server but added to the respective SQL regardless. As a result, requests may receive a response of `2xx` with an empty body instead of a `403`.
38
+ + Until `@sap/cds^9`, this change can be deactivated via `cds.env.features.compat_static_auth = true`
39
+ + Read restrictions on the entity are no longer taken into consideration when evaluating restrictions on bound actions/ functions
40
+ + Until `@sap/cds^9`, this change can be deactivated via `cds.env.features.compat_restrict_bound = true`
41
+
42
+ ### Fixed
43
+
44
+ - ETag calculation if column was provided as Javascript Date
45
+ - Forwarding of `/$count` queries while mocking the external service
46
+ - Resolving of implicit function parameters (e.g `GET .../test.foo?x='bar'`)
47
+ - Arrayed elements are not part of response unless explicitly selected with `$select`
48
+ - In case of nonexistent user attributes (`$user.X`), only the subclause gets substituted with `false`
49
+ - `@odata.context` in new OData adapter:
50
+ + Fixed crash for requests to actions/functions when `cds.env.odata.context_with_columns` is enabled
51
+ + Aggregation functions with `$apply` are now returned when `cds.env.odata.context_with_columns` is enabled
52
+ + `@odata.context` is now the first property in the response values of `concat` requests
53
+ + Binary key values are now properly encoded and formatted
54
+ + Fixed keys appearing as `(undefined)` for updates via navigation to-one
55
+ + Fixed key value pairs being returned as `undefined=undefined` for property access of aspects
56
+ + Backlinks no longer appear as keys for property access of aspects
57
+ + Non-anonymous structured types are now prefixed with the service name
58
+ + Structured types no longer end with `/$entity`
6
59
 
7
60
  ## Version 8.5.1 - 2024-12-06
8
61
 
@@ -522,7 +575,7 @@
522
575
  - Support for `@odata.draft.bypass` to allow direct modifications of active instances.
523
576
  - `req.user.tokenInfo` for `@sap/xssec`-based authentication (`ias`, `jwt`, `xsuaa`)
524
577
  - `cds.fiori.draft_lock_timeout` as successor of `cds.drafts.cancellationTimeout`.
525
- + Possible values are /^([0-9]+)(h|hrs|min)$/ or a number in milliseconds.
578
+ + Possible values are `/^([0-9]+)(h|hrs|min)$/` or a number in milliseconds.
526
579
  - There is a new `sap.common.Timezones` entity with a basic time zone definition. There will be accompanying data in package `@sap/cds-common-content`.
527
580
  - Deprecation warnings for configuration options `cds.drafts.cancellationTimeout`, `cds.features.serve_on_root`, `cds.features.stream_compat`, `cds.fiori.lean_draft` and `cds.requires.middlewares`, as well as for the properties `req.user.locale` and `req.user.tenant`. The deprecation warnings can be turned off by setting `cds.features.deprecated` to `off`.
528
581
 
@@ -44,7 +44,7 @@ Currency=Currency
44
44
  CurrencyCode=Currency Code
45
45
 
46
46
  #XTIT: Currency Code Description
47
- CurrencyCode.Description=A currency code as specified in ISO 4217
47
+ CurrencyCode.Description=Currency code as specified by ISO 4217
48
48
 
49
49
  #XTIT: Currency Symbol
50
50
  CurrencySymbol=Currency Symbol
@@ -59,7 +59,7 @@ Country=Country/Region
59
59
  CountryCode=Country/Region Code
60
60
 
61
61
  #XTIT: Country/Region Code Description
62
- CountryCode.Description=A country/region code as specified in ISO 3166-1
62
+ CountryCode.Description=Country/region code as specified by ISO 3166-1
63
63
 
64
64
  #XTIT: Language
65
65
  Language=Language
@@ -68,10 +68,7 @@ Language=Language
68
68
  LanguageCode=Language Code
69
69
 
70
70
  #XTIT: Language Code Description
71
- LanguageCode.Description=A language code as specified in ISO 639-1
72
-
73
- #XTIT Time zone code
74
- TimeZoneCode=Time Zone Code
71
+ LanguageCode.Description=Language code as specified by ISO 639-1
75
72
 
76
73
  #XTIT: User Identifier
77
74
  UserID=User ID
@@ -83,7 +80,7 @@ Name=Name
83
80
  Description=Description
84
81
 
85
82
  #XTOL: A user's unique Indentifier
86
- UserID.Description=A user's unique ID
83
+ UserID.Description=User's unique ID
87
84
 
88
85
  #XTIT: Admin data for a draft document
89
86
  Draft_DraftAdministrativeData=Draft Administrative Data
package/eslint.config.mjs CHANGED
@@ -27,8 +27,8 @@ export const defaults = {
27
27
  CXL: 'readonly',
28
28
 
29
29
  // subset of Node.js globals ...
30
- __dirname: 'readonly',
31
30
  __filename: 'readonly',
31
+ __dirname: 'readonly',
32
32
  exports: 'writable',
33
33
  require: 'readonly',
34
34
  global: 'readonly',
@@ -1,40 +1,43 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
3
 
4
- const Properties = module.exports = { read, parse }
5
-
6
- function read (res, ext = '.properties') {
4
+ exports.read = function read (res, ext = '.properties', options) {
7
5
  try {
8
6
  // Although properties are actually latin1-encoded, people tend to have
9
7
  // unicode chars in them (e.g.German umlauts), so let's parse in utf-8.
10
8
  if (!/\.(env|.+rc)$/.test(res) && !path.extname(res)) res += ext
11
9
  const src = fs.readFileSync(path.resolve(res),'utf-8')
12
- const properties = src.match(/^\s*{/) ? JSON.parse(src) : Properties.parse(src)
13
- return Object.defineProperty (properties, '_source', {value:res})
10
+ return Object.defineProperty (exports.parse(src,options), '_source', {value:res})
14
11
  } catch (e) {
15
12
  if (e.code !== 'ENOENT') throw new Error (`Corrupt ${ext} file: ${res+ext}`)
16
13
  }
17
14
  }
18
15
 
19
- function parse (props) {
16
+ exports.parse = function parse (props, options) {
17
+ if (props.match(/^\s*{/)) return JSON.parse(props)
20
18
  const lines = props.split(/(?<![\\\r])\r?\n/)
21
19
  const rows = lines.filter(each => !!each.trim()).map(each => each.replace(/\\\r?\n/, '')).map(each => {
22
20
  const index = each.indexOf('=')
23
21
  if (index < 0) return [each, '']
24
22
  return [each.slice(0, index).trim(), each.slice(index + 1).trim()]
25
23
  })
24
+ const val = options?.strings ? string4 : value4
26
25
  const bundle = rows.reduce((all, [key, value]) => {
27
- if (key && !/^\s*[#;]/.test(key)) all[key] = value4(value)
26
+ if (key && !/^\s*[#;]/.test(key)) all[key] = val(value)
28
27
  return all
29
28
  }, {})
30
29
  return bundle
31
30
  }
32
31
 
33
- function value4(raw) {
32
+ function value4 (raw) {
34
33
  if (raw === 'true') return true
35
34
  if (raw === 'false') return false
36
35
  if (raw === '0') return 0
37
- else return Number(raw) || raw
36
+ else return Number(raw) || string4(raw)
37
+ }
38
+
39
+ function string4 (raw) {
40
+ return raw
38
41
  .replace(/''/g,"'") .replace(/^"(.*)"$/,"$1") .replace(/^'(.*)'$/,"$1")
39
42
  .replace(/\\u[\dA-F]{4}/gi, (match) => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)))
40
43
  }
@@ -1,6 +1,7 @@
1
1
  // The only entry point to produce CSNs consumed by the Java Runtime
2
2
 
3
3
  const cds = require ('../../index')
4
+ const TRACE = cds.debug('trace')
4
5
 
5
6
  function _4java (csn,o) {
6
7
  const compile = require ('../cdsc');
@@ -18,9 +19,7 @@ function _4java_tmp (csn,o) { // as long as compile.for.java is not definitely t
18
19
  return _4draft (dsn, o);
19
20
  }
20
21
 
21
- module.exports = function cds_compile_for_java (csn,o) {
22
- if ('_4java' in csn) return csn._4java
23
- // cds.minify (csn) ?
22
+ function _compile_for_java (csn,o) {
24
23
  const dsn = (!cds.env.features._ucsn_) ? cds.compile.for.odata (csn,o||{}) : _4java (csn,o||{});
25
24
  if (dsn.definitions) for (let [name,d] of Object.entries(dsn.definitions)) {
26
25
  // Add @cds.external to external services
@@ -35,3 +34,16 @@ module.exports = function cds_compile_for_java (csn,o) {
35
34
  Object.defineProperty (dsn, '_4java', {value:dsn})
36
35
  return dsn
37
36
  }
37
+
38
+
39
+ module.exports = function cds_compile_for_java (csn,o) {
40
+ if ('_4java' in csn) return csn._4java
41
+ TRACE?.time('cds.compile 4java'.padEnd(22))
42
+ try {
43
+ // csn = cds.minify (csn)
44
+ let result, next = ()=> result ??= _compile_for_java (csn,o)
45
+ cds.emit ('compile.for.runtime', csn, o, next)
46
+ return next() //> in case no handler called next
47
+ }
48
+ finally { TRACE?.timeEnd('cds.compile 4java'.padEnd(22)) }
49
+ }
@@ -19,6 +19,48 @@ function _isCompositionBacklink(e) {
19
19
  }
20
20
  }
21
21
 
22
+
23
+ // NOTE: Keep outside of the function to avoid calling the parser repeatedly
24
+ const { Draft } = cds.linked(`
25
+ entity ActiveEntity { key ID: UUID; }
26
+ entity Draft {
27
+ virtual IsActiveEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
28
+ virtual HasDraftEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
29
+ HasActiveEntity : Boolean; // This should be written !!!
30
+ DraftAdministrativeData : Association to DRAFT.DraftAdministrativeData;
31
+ DraftAdministrativeData_DraftUUID : UUID;
32
+ // SiblingEntity : Association to ActiveEntity; // REVISIT: Why didn't we use a managed assoc here?
33
+ }
34
+ entity DRAFT.DraftAdministrativeData {
35
+ key DraftUUID : UUID;
36
+ LastChangedByUser : String(256); LastChangeDateTime : Timestamp;
37
+ CreatedByUser : String(256); CreationDateTime : Timestamp;
38
+ InProcessByUser : String(256);
39
+ DraftIsCreatedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
40
+ DraftIsProcessedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
41
+ }
42
+ `).definitions
43
+
44
+
45
+ function DraftEntity4 (active, name = active.name+'.drafts') {
46
+
47
+ const draft = Object.create (active, {
48
+ name: { value: name }, // REVISIT: lots of things break if we do that!
49
+ elements: { value: { ...active.elements, ...Draft.elements }, enumerable: true },
50
+ actives: { value: active },
51
+ query: { value: undefined }, // to not inherit that from active
52
+ // drafts: { value: undefined }, // to not inherit that from active -> doesn't work yet as the coding in lean-draft.js uses .drafts to identify both active and draft entities
53
+ isDraft: { value: true },
54
+ })
55
+
56
+ // for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
57
+ const _pname = active['@cds.persistence.name']
58
+ if (_pname) draft['@cds.persistence.name'] = _pname + '_drafts'
59
+
60
+ return draft
61
+ }
62
+
63
+
22
64
  module.exports = function cds_compile_for_lean_drafts(csn) {
23
65
  function _redirect(assoc, target) {
24
66
  assoc.target = target.name
@@ -26,54 +68,22 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
26
68
  }
27
69
 
28
70
  function _isDraft(def) {
71
+ // return 'DraftAdministrativeData' in def.elements
29
72
  return (
30
73
  def.associations?.DraftAdministrativeData ||
31
74
  (def.own('@odata.draft.enabled') && def.own('@Common.DraftRoot.ActivationAction'))
32
75
  )
33
76
  }
34
77
 
35
- const { Draft } = cds.linked(`
36
- entity ActiveEntity { key ID: UUID; }
37
- entity Draft {
38
- virtual IsActiveEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
39
- virtual HasDraftEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
40
- HasActiveEntity : Boolean; // This should be written !!!
41
- DraftAdministrativeData : Association to DRAFT.DraftAdministrativeData;
42
- DraftAdministrativeData_DraftUUID : UUID;
43
- // SiblingEntity : Association to ActiveEntity; // REVISIT: Why didn't we use a managed assoc here?
44
- }
45
- entity DRAFT.DraftAdministrativeData {
46
- key DraftUUID : UUID;
47
- LastChangedByUser : String(256); LastChangeDateTime : Timestamp;
48
- CreatedByUser : String(256); CreationDateTime : Timestamp;
49
- InProcessByUser : String(256);
50
- DraftIsCreatedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
51
- DraftIsProcessedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
52
- }
53
- `).definitions
54
-
55
78
  function addDraftEntity(active, model) {
56
79
  const _draftEntity = active.name + '.drafts'
57
80
  const d = model.definitions[_draftEntity]
58
81
  if (d) return d
59
82
  // We need to construct a fake draft entity definition
60
83
  // We cannot use new cds.entity because runtime aspects would be missing
61
- const draft = {
62
- __proto__: active,
63
- name: _draftEntity,
64
- elements: { ...active.elements, ...Draft.elements },
65
- query: undefined
66
- }
67
-
68
- // for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
69
- if (active['@cds.persistence.name'])
70
- draft['@cds.persistence.name'] = active['@cds.persistence.name'] + '_drafts'
71
-
84
+ const draft = new DraftEntity4 (active, _draftEntity)
72
85
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
73
86
  Object.defineProperty(active, 'drafts', { value: draft })
74
- Object.defineProperty(active, 'actives', { value: active })
75
- Object.defineProperty(draft, 'actives', { value: active })
76
- Object.defineProperty(draft, 'isDraft', { value: true })
77
87
 
78
88
  // Positive list would be bigger (search, requires, fiori, ...)
79
89
  if (draft['@readonly']) draft['@readonly'] = undefined
@@ -2,16 +2,25 @@ const { unfold_csn } = require ('../etc/_localized')
2
2
  const cds = require ('../../index')
3
3
  const TRACE = cds.debug('trace')
4
4
 
5
- module.exports = function cds_compile_for_nodejs (csn,o) {
6
- if ('_4nodejs' in csn) return csn._4nodejs
7
- TRACE?.time('cds.compile 4nodejs'.padEnd(22))
8
- let dsn = csn // cds.minify (csn)
9
- dsn = cds.compile.for.odata (csn,o) //> creates a partial copy -> avoid any cds.linked() before
10
- dsn = unfold_csn (dsn)
11
- dsn = cds.linked (dsn)
5
+
6
+ function _compile_for_nodejs (csn, o) {
7
+ let min = cds.minify (csn)
8
+ let dsn = cds.compile.for.odata(min, o) //> creates a partial copy -> avoid any cds.linked() before
9
+ dsn = unfold_csn(dsn)
10
+ dsn = cds.linked(dsn)
12
11
  cds.compile.for.lean_drafts(dsn, o)
13
- Object.defineProperty (csn, '_4nodejs', {value:dsn})
14
- Object.defineProperty (dsn, '_4nodejs', {value:dsn})
15
- TRACE?.timeEnd('cds.compile 4nodejs'.padEnd(22))
12
+ Object.defineProperty(csn, '_4nodejs', { value: dsn })
13
+ Object.defineProperty(dsn, '_4nodejs', { value: dsn })
16
14
  return dsn
17
15
  }
16
+
17
+
18
+ module.exports = function cds_compile_for_nodejs (csn,o) {
19
+ if ('_4nodejs' in csn) return csn._4nodejs
20
+ TRACE?.time('cds.compile 4nodejs'.padEnd(22)); try {
21
+ let result, next = ()=> result ??= _compile_for_nodejs (csn,o)
22
+ cds.emit ('compile.for.runtime', csn, o, next)
23
+ return next() //> in case no handler called next
24
+ }
25
+ finally { TRACE?.timeEnd('cds.compile 4nodejs'.padEnd(22)) }
26
+ }
@@ -3,7 +3,7 @@ const DEBUG = cds.debug('minify')
3
3
 
4
4
  module.exports = function cds_minify (csn, roots = cds.env.features.skip_unused) {
5
5
  if (roots === false) return csn
6
- if (csn[_minified]) return csn
6
+ if ((csn.meta??={}).minified) return csn
7
7
  const all = csn.definitions, reached = new Set
8
8
  if (roots === 'services') {
9
9
  for (let n in all) if (all[n].kind === 'service') _visit_service(n)
@@ -58,8 +58,6 @@ module.exports = function cds_minify (csn, roots = cds.env.features.skip_unused)
58
58
  const minified = csn, less = minified.definitions = {}
59
59
  for (let n in all) if (reached.has(all[n])) less[n] = all[n]
60
60
  else DEBUG?.('skipping', all[n].kind, n)
61
- Object.defineProperty (minified,_minified,{value:true})
61
+ ;(minified.meta??={}).minified = true
62
62
  return minified
63
63
  }
64
-
65
- const _minified = Symbol('minified')
@@ -1,86 +1,120 @@
1
1
  const cdsc = require ('@sap/cds-compiler')
2
2
  const cds = require ('../index')
3
3
 
4
- /** cds.parse is both, a namespace and a shortcut for cds.parse.cdl */
5
- const cds_parse = (src,o) => cds.compile (src,o,'parsed')
6
- const parse = module.exports = Object.assign (cds_parse, {
4
+ const parse = module.exports = exports = (...args) => parse.cdl (...args)
7
5
 
8
- CDL: (...args) => tagged(parse.cdl, ...args),
9
- CQL: (...args) => tagged(parse.cql, ...args),
10
- CXL: (...args) => tagged(parse.expr, ...args),
6
+ exports.cdl = function cdl (x,...etc) {
7
+ if (x.raw) return parse.ttl (cdl, x,...etc)
8
+ else return cds.compile (x, etc[0], 'parsed')
9
+ }
10
+
11
+ exports.cql = function cql (x,...etc) { try {
12
+ if (x.raw) return parse.ttl (cql, x,...etc)
13
+ // add missing 'from' clause if not present -> REVISIT: this is a hack unless compiler accepts partial cql
14
+ const from = x.match(/ from /i); if (!from) x = x.replace(/( where | having | group by | order by | limit |$)/i, x => ' from $' + x)
15
+ const cqn = cdsc.parse.cql(x,undefined,{ messages:[], ...etc[0] })
16
+ if (!from) delete cqn.SELECT.from
17
+ return cqn
18
+ } catch(e) {
19
+ // cds-compiler v5 does not put messages into `e.message` anymore; render them explicitly
20
+ e.message = !e.messages ? e.message : e.toString();
21
+ e.message = e.message.replace('<query>.cds:',`In '${e.cql = x}' at `)
22
+ throw e // with improved error message
23
+ }}
11
24
 
12
- cdl: cds_parse,
13
- cql: (x,o) => { try { return cdsc.parse.cql(x,undefined,{ messages:[], ...o }) } catch(e) {
25
+ exports.path = function path (x,...etc) {
26
+ if (x.raw) return parse.ttl (path, x,...etc)
27
+ if (cds.model?.definitions[x]) return {ref:[x]}
28
+ if (/^([\w_.$]+)$/.test(x)) return {ref:[x]} // optimized parsing of simple paths of length 1
29
+ const [,head,tail] = /^([\w._]+)(?::(\w+))?$/.exec(x)||[]
30
+ if (tail) return {ref:[head,...tail.split('.')]}
31
+ if (head) return {ref:[head]}
32
+ const {SELECT} = cdsc.parse.cql('SELECT from '+x)
33
+ return SELECT.from
34
+ }
35
+
36
+ exports.expr = function expr (x,...etc) {
37
+ if (x.raw) return parse.ttl (expr, x,...etc)
38
+ if (typeof x !== 'string') throw cds.error.expected `${{x}} to be an expression string`
39
+ if (x in globals) return globals[x] // optimized parsing of true, false, null
40
+ if (/^([\d.]+)$/.test(x)) return {val:Number(x)} // optimized parsing of numeric vals
41
+ if (/^([\w_$]+)$/.test(x)) return {ref:[x]} // optimized parsing of simple refs of length 1
42
+ if (/^([\w_.$]+)$/.test(x)) return {ref:x.split('.')} // optimized parsing of simple refs of length > 1
43
+ try { return cdsc.parse.expr(x,undefined,{ messages:[], ...etc[0] }) } catch(e) {
14
44
  // cds-compiler v5 does not put messages into `e.message` anymore; render them explicitly
15
45
  e.message = !e.messages ? e.message : e.toString();
16
- e.message = e.message.replace('<query>.cds:',`In '${e.cql = x}' at `)
46
+ e.message = e.message.replace('<expr>.cds:1:',`In '${e.expr = x}' at `)
17
47
  throw e // with improved error message
18
- }},
19
- path: (x,...values) => {
20
- if (x && x.raw) return tagged (parse.path,x,...values)
21
- if (/^[A-Za-z_0-9.$]*$/.test(x)) return {ref:[x]}
22
- if ((cds.context?.model || cds.model)?.definitions[x]) return {ref:[x]}
23
- let {SELECT} = parse.cql('SELECT from '+x)
24
- return SELECT.from
25
- },
26
- column: x => {
27
- let as = /\s+as\s+(\w+)$/i.exec(x)
28
- if (as) {
29
- let col = parse.expr(x.slice(0,as.index)); col.as = as[1]
30
- return col
31
- }
32
- else return parse.expr(x)
33
- },
34
- expr: (x,o) => {
35
- if (typeof x !== 'string') throw cds.error.expected `${{x}} to be an expression string`
36
- if (x in keywords) return {ref:[x]}
37
- try { return cdsc.parse.expr(x,undefined,{ messages:[], ...o }) } catch(e) {
38
- // cds-compiler v5 does not put messages into `e.message` anymore; render them explicitly
39
- e.message = !e.messages ? e.message : e.toString();
40
- e.message = e.message.replace('<expr>.cds:1:',`In '${e.expr = x}' at `)
41
- throw e // with improved error message
42
- }
43
- },
44
- xpr: x => { const y = parse.expr(x); return y.xpr || [y] },
45
- ref: x => parse.expr(x).ref,
46
-
47
- properties: (...args) => (parse.properties = require('./etc/properties').parse) (...args),
48
- yaml: (...args) => (parse.yaml = require('./etc/yaml').parse) (...args),
49
- csv: (...args) => (parse.csv = require('./etc/csv').parse) (...args),
50
- json: (...args) => JSON.parse (...args),
51
-
52
- })
53
-
54
-
55
- const tagged = (parse, strings, ...values) => {
56
- if (!strings.raw) return parse (strings, ...values)
57
- if (!strings.length) return parse (strings[0])
58
- const all = new Array (strings.length + values.length)
59
- for (var i=0; i<strings.length-1; ++i) {
60
- let v = values[i], s = strings[i]
61
- all[2*i] = s
62
- all[2*i+1] = v instanceof cds.entity ? v.name : ':'+i
63
- if (typeof v === 'string' && s.endsWith(' like ') && !v.includes('%')) values[i] = `%${v}%`
64
- if (Array.isArray(v) && s.toLowerCase().endsWith(' in ')) values[i] = {list: v.map(cxn4)}
65
48
  }
66
- all[2*i] = strings[i]
67
- return merge (parse(all.join('')), values)
68
49
  }
69
50
 
70
- const merge = (o,values) => {
71
- for (let k in o) {
72
- const x = o[k]
73
- if (!x) continue
74
- if (x.param) {
75
- let val = values[x.ref[0]]; if (val === undefined) continue
76
- let y = o[k] = cxn4(val)
77
- if (x.cast) y.cast = x.cast
78
- if (x.key) y.key = x.key
79
- if (x.as) y.as = x.as
80
- } else if (typeof x === 'object') merge(x,values)
51
+ exports.xpr = (...args) => {
52
+ const y = parse.expr (...args)
53
+ return y.xpr || [y]
54
+ }
55
+
56
+ exports.ref = (x,...etc) => {
57
+ const ntf = {null:{val:null},true:{val:true},false:{val:false}}[x]; if (ntf) return ntf
58
+ if (/^[A-Za-z_$][\w.]*$/.test(x)) return { ref: x.split('.') }
59
+ else return parse.expr (x,...etc)
60
+ }
61
+
62
+ exports.properties = (...args) => (parse.properties = require('./etc/properties').parse) (...args)
63
+ exports.yaml = (...args) => (parse.yaml = require('./etc/yaml').parse) (...args)
64
+ exports.csv = (...args) => (parse.csv = require('./etc/csv').parse) (...args)
65
+ exports.json = (...args) => JSON.parse (...args)
66
+
67
+
68
+ exports.ttl = (parse, strings, ...values) => {
69
+
70
+ // Reusing cached results for identical template strings w/o values
71
+ // if (!values.length) {
72
+ // const cache = _parse_ttl.cache ??= new WeakMap
73
+ // if (cache.has(strings)) return cache.get(strings)
74
+ // let parsed = parse (strings[0])
75
+ // cache.set(strings,parsed)
76
+ // return parsed
77
+ // }
78
+
79
+ let cql = values.reduce ((cql,v,i) => {
80
+ if (Array.isArray(v) && strings[i].match(/ in $/i)) values[i] = { list: v.map(cxn4) }
81
+ return cql + strings[i] + (v instanceof cds.entity ? v.name : ':'+i)
82
+ },'') + strings.at(-1)
83
+ const cqn = parse (cql) //; cqn.$params = values
84
+ return merge (cqn, values)
85
+
86
+ function merge (o,values) {
87
+ for (let k in o) {
88
+ const x = o[k]
89
+ if (!x) continue
90
+ if (x.param) {
91
+ let val = values[x.ref[0]]; if (val === undefined) continue
92
+ let y = o[k] = cxn4(val) //; y.$ = x.ref[0]
93
+ if (x.cast) y.cast = x.cast
94
+ if (x.key) y.key = x.key
95
+ if (x.as) y.as = x.as
96
+ } else if (typeof x === 'object') merge(x,values)
97
+ }
98
+ return o
81
99
  }
82
- return o
83
100
  }
84
101
 
85
- const cxn4 = x => x.SELECT || x.ref || x.xpr || x.val !== undefined|| x.list || x.func ? x : {val:x}
86
- const keywords = { KEY:1, key:1, SELECT:1, select:1 }
102
+
103
+ /** @protected */
104
+ exports._select = (prefix, ttl, suffix) => {
105
+ let [ strings, ...values ] = ttl; strings = [...strings]; strings.raw = strings; // need that as ttl strings are sealed
106
+ if (prefix) strings[0] = `SELECT ${prefix} ${strings[0]}`; else strings[0] = `SELECT ${strings[0]}`
107
+ if (suffix) strings[strings.length-1] += ` ${suffix}`
108
+ return cds.parse.cql (strings, ...values).SELECT
109
+ }
110
+
111
+ const is_cqn = x => typeof x === 'object' && (
112
+ 'ref' in x ||
113
+ 'val' in x ||
114
+ 'xpr' in x ||
115
+ 'list' in x ||
116
+ 'func' in x ||
117
+ 'SELECT' in x
118
+ )
119
+ const cxn4 = x => is_cqn(x) ? x : {val:x}
120
+ const globals = { true: {val:true}, false: {val:false}, null: {val:null} }
@@ -1,5 +1,6 @@
1
1
  const cdsc = require ('../cdsc')
2
2
  const cds = require ('../../index')
3
+ const TRACE = cds.debug('trace')
3
4
 
4
5
  if (cds.env.features.precompile_edms !== false) {
5
6
  const _precompiled = new WeakMap
@@ -18,21 +19,30 @@ if (cds.env.features.precompile_edms !== false) {
18
19
  function cds_compile_to_edm (csn,_o) {
19
20
  const o = cdsc._options.for.edm(_o) //> used twice below...
20
21
  csn = _4odata(csn,o)
21
- if (o.service === 'all') return _many ('.json',
22
- cdsc.to.edm.all (csn,o),
23
- o.as === 'str' ? JSON.stringify : x=>x
24
- )
25
- else return cdsc.to.edm (csn,o)
22
+ TRACE?.time('cds.compile 2edm'.padEnd(22))
23
+ try {
24
+ let result, next = ()=> result = o.service === 'all' ? _many ('.json',
25
+ cdsc.to.edm.all (csn,o), o.as === 'str' ? JSON.stringify : x=>x
26
+ ) : cdsc.to.edm (csn,o)
27
+ cds.emit ('compile.to.edmx', csn, o, next) // NOTE: intentionally using same event as for edmx
28
+ return result ??= next() //> in case no handler called next
29
+ }
30
+ finally { TRACE?.timeEnd('cds.compile 2edm'.padEnd(22)) }
26
31
  }
27
32
 
28
33
 
29
34
  function cds_compile_to_edmx (csn,_o) {
30
35
  const o = cdsc._options.for.edm(_o) //> used twice below...
31
36
  csn = _4odata(csn,o)
32
- if (o.service === 'all') return _many ('.xml',
33
- cdsc.to.edmx.all (csn,o)
34
- )
35
- else return cdsc.to.edmx (csn,o)
37
+ TRACE?.time('cds.compile 2edmx'.padEnd(22))
38
+ try {
39
+ let result, next = ()=> result ??= o.service === 'all' ? _many ('.xml',
40
+ cdsc.to.edmx.all (csn,o)
41
+ ) : cdsc.to.edmx (csn,o)
42
+ cds.emit ('compile.to.edmx', csn, o, next)
43
+ return next() //> in case no handler called next
44
+ }
45
+ finally { TRACE?.timeEnd('cds.compile 2edmx'.padEnd(22)) }
36
46
  }
37
47
 
38
48