@sap/cds 8.2.1 → 8.2.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 8.2.2 - 2024-09-13
8
+
9
+ ### Fixed
10
+
11
+ - Erroneous caching in `cds.validate`
12
+ - Properly check `$filter` element types across navigations
13
+
7
14
  ## Version 8.2.1 - 2024-09-04
8
15
 
9
16
  ### Fixed
@@ -92,43 +92,54 @@ const $any = class any {
92
92
  * this method for subsequent usages, with statically determined checks.
93
93
  */
94
94
  check_asserts (val, path, /** @type {Validation} */ ctx) {
95
- // if (this['@cds.validate'] === false) return this.set ('check_asserts', ()=>{})
96
- const asserts = []
97
- const type_check = conf.strict && this.strict_check || this.type_check
98
- if (type_check) {
99
- asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, v, this ))
100
- }
101
- if (this._is_mandatory()) {
102
- asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
103
- }
104
- if (this['@assert.format']) {
105
- const format = new RegExp(this['@assert.format'],'u')
106
- asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, v, format))
107
- }
108
- if (this['@assert.range'] && !this.enum) {
109
- const [ min, max ] = this['@assert.range']
110
- asserts.push ((v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max))
111
- }
112
- if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
113
- const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
114
- const enums = vals.reduce((a,v) => (a[v]=true, a),{})
115
- asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
116
- }
117
- if (!asserts.length) return this.check_asserts = ()=>{} // nothing to do
118
- this.set ('check_asserts', (v,p,ctx) => asserts.forEach (a => a(v,p,ctx)))
119
- this.check_asserts (val, path, ctx) // call first time
95
+ // IMPORTANT: We need to use this.own() here as elements derived from reuse
96
+ // definitions or from elements of base entities might have different asserts
97
+ // than inherited ones.
98
+ const check_asserts = this.own('_check_asserts', () => {
99
+ const asserts = []
100
+ const type_check = conf.strict && this.strict_check || this.type_check
101
+ if (type_check) {
102
+ asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, v, this ))
103
+ }
104
+ if (this._is_mandatory()) {
105
+ asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
106
+ }
107
+ if (this['@assert.format']) {
108
+ const format = new RegExp(this['@assert.format'],'u')
109
+ asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, v, format))
110
+ }
111
+ if (this['@assert.range'] && !this.enum) {
112
+ const [ min, max ] = this['@assert.range']
113
+ asserts.push ((v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, v, min, max))
114
+ }
115
+ if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
116
+ const vals = Object.entries(this.enum).map(([k,v]) => 'val' in v ? v.val : k)
117
+ const enums = vals.reduce((a,v) => (a[v]=true, a),{})
118
+ asserts.push ((v,p,ctx) => v == null || v in enums || vals.some(x => x == v) || ctx.error ('ASSERT_ENUM', p, this.name, typeof v === 'string' ? `"${v}"` : v, vals.join(', ')))
119
+ }
120
+ if (!asserts.length) return ()=>{} // nothing to do
121
+ return (v,p,ctx) => asserts.forEach (a => a(v,p,ctx))
122
+ })
123
+ return check_asserts (val, path, ctx)
120
124
  }
121
125
 
122
126
  _is_mandatory (d=this) {
123
- return d.own('_mandatory', ()=> (
124
- !d['@readonly'] // readonly -> not mandatory
125
- && (d['@mandatory'] || d['@Common.FieldControl']?.['#'] === 'Mandatory')
126
- && !d._is_flattened()
127
- ))
128
- }
129
-
130
- _is_flattened (d=this) {
131
- return d.parent?.query?.SELECT.columns?.some (c => c.ref?.length > 1 && d.name === (c.as || c.ref.at(-1)))
127
+ return d.own('_mandatory', ()=> {
128
+ if (d._is_readonly()) return false // readonly annotations have precedence over mandatory ones
129
+ if (d['@mandatory'] || d['@Common.FieldControl']?.['#'] === 'Mandatory') {
130
+ const q = d.parent?.query?.SELECT
131
+ if (!q) return true // it's a regular entity's element marked as mandatory
132
+ if (!q.from?.ref) return false // join or union -> elements can't be mandatory
133
+ const c = q.columns?.find (c => alias4(c) === d.name)
134
+ if (!c) return true // * or foo.* -> can't tell whether d is joined
135
+ if (!c.ref) return false // calculated fields aren't mandatory
136
+ if (c.ref.length === 1) return true // SELECT from Foo { foo }
137
+ if (c.ref.length === 2 && c.ref[0] === alias4(q.from)) return true // SELECT from Foo as f { f.foo }
138
+ else return false // joined field which can't be mandatory, e.g. SELECT from Books { author.name as author }
139
+ function alias4 (x) { return x.as || x.ref?.at(-1) }
140
+ }
141
+ else return false
142
+ })
132
143
  }
133
144
 
134
145
  _is_readonly (d=this) {
@@ -148,17 +159,21 @@ const $any = class any {
148
159
  * This is the case if the row date does not contain all primary key elements of the target entity.
149
160
  */
150
161
  _is_insert (row) {
151
- const entity = this._target || this
152
- const keys = Object.keys (entity.keys||{})
153
- if (!keys.length) return this.set('_is_insert', ()=> true), true
154
- else this.set('_is_insert', data => typeof data === 'object' && !keys.every(k => k in data))
155
- return this._is_insert (row)
162
+ // IMPORTANT: We need to use this.own() here as derived entities might have
163
+ // different keys and thus different insert checks.
164
+ const _is_insert = this.own('__is_insert', () => {
165
+ const entity = this._target || this
166
+ const keys = Object.keys (entity.keys||{})
167
+ if (!keys.length) return ()=> true
168
+ else return data => typeof data === 'object' && !keys.every(k => k in data)
169
+ })
170
+ return _is_insert(row)
156
171
  }
157
172
 
158
173
  _required (elements) {
159
- const _required = Object.values(elements).filter(this._is_mandatory)
160
- this.set('_required', ()=> _required)
161
- return _required
174
+ // IMPORTANT: We need to use this.own() here as derived entities might have
175
+ // different elements or elements with different annotations than base entitites.
176
+ return this.own('__required', ()=> Object.values(elements).filter(this._is_mandatory))
162
177
  }
163
178
 
164
179
  /** Forward declaration for universal CSN */
@@ -754,7 +754,7 @@ module.exports = (cqn, model, namespace, protocol) => {
754
754
  }
755
755
 
756
756
  if (cqn.SELECT.where) {
757
- _processWhere(cqn.SELECT.where, root)
757
+ _processWhere(cqn.SELECT.where, target)
758
758
  }
759
759
 
760
760
  // one?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "8.2.1",
3
+ "version": "8.2.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [