@sap/cds 7.4.0 → 7.4.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
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 7.4.1 - 2023-11-23
8
+
9
+ ### Fixed
10
+
11
+ - Add dynamic properties to result when experimental feature `cds.env.features.okra_skip_query_options` is active
12
+ - Allow negative integers in new parser
13
+ - Allow deletion of instances outside the draft tree
14
+ - Tenant lookup in OData metadata requests
15
+ - `cds.parse.csv` and `cds deploy` correctly parse CSV files with Windows file endings (CRLF) and quoted values
16
+ - Typescript Typings
17
+
7
18
  ## Version 7.4.0 - 2023-11-13
8
19
 
9
20
  ### Added
package/apis/core.d.ts CHANGED
@@ -3,13 +3,13 @@ import * as csn from './csn'
3
3
  import { service } from './server'
4
4
 
5
5
  // These are classes actually -> using the new() => interface trick
6
- export type Association = new() => LinkedAssociation
7
- export type Composition = new() => LinkedAssociation
8
- export type entity = new() => LinkedEntity
9
- export type event = new() => linked & csn.struct
10
- export type type = new() => linked & csn.type
11
- export type array = new() => linked & csn.type
12
- export type struct = new() => linked & csn.struct
6
+ export type Association = new(_?:object) => LinkedAssociation
7
+ export type Composition = new(_?:object) => LinkedAssociation
8
+ export type entity = new(_?:object) => LinkedEntity
9
+ export type event = new(_?:object) => linked & csn.struct
10
+ export type type = new(_?:object) => linked & csn.type
11
+ export type array = new(_?:object) => linked & csn.type
12
+ export type struct = new(_?:object) => linked & csn.struct
13
13
 
14
14
  export default class cds {
15
15
  // infer (query : cqn, model : csn) : LinkedDefinition
package/apis/events.d.ts CHANGED
@@ -34,6 +34,7 @@ export default class cds {
34
34
  * @see [capire docs](https://cap.cloud.sap/docs/node.js/events)
35
35
  */
36
36
  export class EventContext {
37
+ constructor(properties:{event:string, data?:object, query?:object, headers:object});
37
38
  http?: {req: express.Request, res: express.Response}
38
39
  tenant: string
39
40
  user: User
@@ -48,7 +49,7 @@ export class EventContext {
48
49
  export class Event extends EventContext {
49
50
  event: string
50
51
  data: any
51
- headers: {}
52
+ headers: any
52
53
  }
53
54
 
54
55
  /**
package/apis/linked.d.ts CHANGED
@@ -9,7 +9,9 @@ export interface linked {
9
9
  }
10
10
 
11
11
  interface LinkedEntity extends linked, entity {
12
+ constructor (properties: object)
12
13
  keys: Definitions
14
+ drafts: LinkedEntity
13
15
  }
14
16
 
15
17
  interface LinkedAssociation extends linked, Association {
package/apis/ql.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { CSN, Definition, EntityElements } from "./csn"
2
2
  import * as CQN from "./cqn"
3
3
  import { Constructable, ArrayConstructable, SingularType } from "./internal/inference"
4
+ import { LinkedEntity } from "./linked"
4
5
 
5
6
  export type Query = CQN.Query
6
7
 
@@ -206,6 +207,7 @@ TaggedTemplateQueryPart<Awaitable<SELECT<unknown>, InstanceType<any>>>
206
207
  => Awaitable<SELECT<SingularType<T>>, SingularType<T>>)
207
208
 
208
209
  & ((entity: Definition | string, primaryKey? : PK, projection? : Projection<unknown>) => SELECT<any>)
210
+ & ((entity: LinkedEntity | string, primaryKey? : PK, projection? : Projection<unknown>) => SELECT<any>)
209
211
  & (<T> (entity: T[], projection? : Projection<T>) => Awaitable<SELECT<T>, T>)
210
212
  & (<T> (entity: T[], primaryKey : PK, projection? : Projection<T>) => Awaitable<SELECT<T>, T>)
211
213
  & (<T> (entity: {new():T}, projection? : Projection<T>) => Awaitable<SELECT<T>, T>)
@@ -225,6 +227,7 @@ type SELECT_from =
225
227
  => Awaitable<SELECT<SingularType<T>>, InstanceType<SingularType<T>>>) // when specifying a key, we expect a single element as result
226
228
  // calling with definition
227
229
  & ((entity: Definition | string, primaryKey? : PK, projection? : Projection<unknown>) => SELECT<any>)
230
+ & ((entity: LinkedEntity | string, primaryKey? : PK, projection? : Projection<unknown>) => SELECT<any>)
228
231
  // calling with concrete list
229
232
  & (<T> (entity: T[], projection? : Projection<T>) => SELECT<T> & Promise<T[]>)
230
233
  & (<T> (entity: T[], primaryKey : PK, projection? : Projection<T>) => Awaitable<SELECT<T>, T>)
@@ -234,6 +237,7 @@ export class INSERT<T> extends ConstructedQuery {
234
237
  static into : (<T extends ArrayConstructable<any>> (entity:T, entries? : object | object[]) => INSERT<SingularType<T>>)
235
238
  & (TaggedTemplateQueryPart<INSERT<unknown>>)
236
239
  & ((entity : Definition | string, entries? : object | object[]) => INSERT<any>)
240
+ & ((entity : LinkedEntity | string, entries? : object | object[]) => INSERT<any>)
237
241
  & (<T> (entity:Constructable<T>, entries? : object | object[]) => INSERT<T>)
238
242
  & (<T> (entity:T, entries? : T | object | object[]) => INSERT<T>)
239
243
 
@@ -255,6 +259,7 @@ export class UPSERT<T> extends ConstructedQuery {
255
259
  static into : (<T extends ArrayConstructable<any>> (entity:T, entries? : object | object[]) => UPSERT<SingularType<T>>)
256
260
  & (TaggedTemplateQueryPart<UPSERT<unknown>>)
257
261
  & ((entity : Definition | string, entries? : object | object[]) => UPSERT<any>)
262
+ & ((entity : LinkedEntity | string, entries? : object | object[]) => UPSERT<any>)
258
263
  & (<T> (entity:Constructable<T>, entries? : object | object[]) => UPSERT<T>)
259
264
  & (<T> (entity:T, entries? : T | object | object[]) => UPSERT<T>)
260
265
 
@@ -289,6 +294,7 @@ export class UPDATE<T> extends ConstructedQuery {
289
294
  static entity <T extends ArrayConstructable<any>> (entity:T, primaryKey? : PK) : UPDATE<SingularType<T>>
290
295
 
291
296
  static entity (entity : Definition | string, primaryKey? : PK) : UPDATE<any>
297
+ static entity (entity : LinkedEntity | string, primaryKey? : PK) : UPDATE<any>
292
298
  static entity <T> (entity:Constructable<T>, primaryKey? : PK) : UPDATE<T>
293
299
  static entity <T> (entity:T, primaryKey? : PK) : UPDATE<T>
294
300
  byKey (primaryKey? : PK) : this
@@ -307,10 +313,12 @@ export class UPDATE<T> extends ConstructedQuery {
307
313
 
308
314
  export class CREATE<T> extends ConstructedQuery {
309
315
  static entity (entity : Definition | string) : CREATE<any>
316
+ static entity (entity : LinkedEntity | string) : CREATE<any>
310
317
  CREATE : CQN.CREATE["CREATE"]
311
318
  }
312
319
 
313
320
  export class DROP<T> extends ConstructedQuery {
314
321
  static entity (entity : Definition | string) : DROP<any>
322
+ static entity (entity : LinkedEntity | string) : DROP<any>
315
323
  DROP : CQN.DROP["DROP"]
316
324
  }
package/apis/server.d.ts CHANGED
@@ -100,18 +100,18 @@ export default class cds {
100
100
 
101
101
  }
102
102
 
103
- export type service = any & {
103
+ export type service = {
104
104
  /**
105
105
  * Dummy wrapper for service implementation functions.
106
106
  * Use that in modules to get IntelliSense.
107
107
  */
108
108
  impl (impl: ServiceImpl) : typeof impl
109
- impl <T> (srv:T, impl: ( this: T, srv: (T) ) => any) : typeof impl
109
+ // impl <T> (srv:T, impl: ( this: T, srv: (T) ) => any) : typeof impl
110
110
 
111
111
  /**
112
112
  * Array of all services constructed.
113
113
  */
114
- providers : Service
114
+ providers : Service[]
115
115
  }
116
116
 
117
117
 
@@ -1,7 +1,7 @@
1
1
  import { SELECT, INSERT, UPDATE, DELETE, Query, ConstructedQuery, UPSERT } from './ql'
2
2
  import { Awaitable } from './ql'
3
3
  import { ArrayConstructable, Constructable } from './internal/inference'
4
- import { LinkedCSN, LinkedDefinition, Definitions } from './linked'
4
+ import { LinkedCSN, LinkedDefinition, Definitions, LinkedEntity } from './linked'
5
5
  import { CSN } from './csn'
6
6
  import { EventContext } from './events'
7
7
  import { Request } from './events'
@@ -120,9 +120,9 @@ export class QueryAPI {
120
120
  */
121
121
  export class Service extends QueryAPI {
122
122
  constructor(
123
- name: string,
124
- model: CSN,
125
- options: {
123
+ name?: string,
124
+ model?: CSN,
125
+ options?: {
126
126
  kind: string
127
127
  impl: string | ServiceImpl
128
128
  }
@@ -380,5 +380,5 @@ declare namespace types {
380
380
  | 'NEW' | 'EDIT' | 'PATCH' | 'SAVE'
381
381
  | 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE'
382
382
  | 'COMMIT' | 'ROLLBACK'
383
- type target = string | LinkedDefinition | ArrayConstructable<any>
383
+ type target = string | LinkedDefinition | LinkedEntity | (string | LinkedDefinition | LinkedEntity)[] | ArrayConstructable<any>
384
384
  }
package/bin/serve.js CHANGED
@@ -192,7 +192,7 @@ async function serve (all=[], o={}) {
192
192
  const server = await cds_server(o)
193
193
 
194
194
  // increase keep-alive timeout for CF (gorouter wants >90s)
195
- if (process.env.VCAP_APPLICATION?.cf_api) server.keepAliveTimeout = 91 * 1000
195
+ if (process.env.CF_INSTANCE_GUID) server.keepAliveTimeout = 91 * 1000
196
196
 
197
197
  // return a promise which resolves to the created http server when listening
198
198
  return cds.server.listening = new Promise ((_resolve,_reject) => {
@@ -12,7 +12,8 @@ function read (res) {
12
12
  function parse (csv) {
13
13
  if (csv[0] === BOM) csv = csv.slice(1)
14
14
  let sep
15
- const lines = csv.split('\n')
15
+ // this also means that \r\n within quotes is NOT retained but normalized to \n. We accept this for now.
16
+ const lines = csv.split(/\r?\n/)
16
17
  const rows = [], headers = []
17
18
 
18
19
  let val, values=[]
@@ -21,7 +21,7 @@ const mpSupportsEmptyLocale = () => {
21
21
  const metadata = service => {
22
22
  return async (odataReq, odataRes, next) => {
23
23
  const req = odataReq.getIncomingRequest()
24
- const tenant = req.user && req.user.tenant
24
+ const tenant = req.tenant ?? req.user?.tenant
25
25
  // REVISIT: can we take locale from user, or is there some odata special wrt metadata?
26
26
  const locale = odataRes.getContract().getLocale()
27
27
 
@@ -212,7 +212,14 @@ class TrustedResourceJsonSerializer {
212
212
  _serializeEntity (result, entityType, data, expandItems, odataPath, structurePath) {
213
213
  this._serializeAnnotations(result, data, MetaProperties.ETAG, MetaProperties.CONTEXT)
214
214
 
215
- this._serializeStructure(result, entityType, data, expandItems, odataPath, structurePath || [])
215
+ const { okra_skip_query_options, odata_new_parser } = cds.env.features
216
+ if (okra_skip_query_options && odata_new_parser) {
217
+ Object.entries(data).forEach(([key, value]) => {
218
+ if (!key.startsWith('*')) result[key] = value
219
+ })
220
+ } else {
221
+ this._serializeStructure(result, entityType, data, expandItems, odataPath, structurePath || [])
222
+ }
216
223
 
217
224
  // Add annotation '"@odata.id": null' for an entity of a transient type if some properties have been
218
225
  // aggregated away or if not all of the base type's properties have been serialized.
@@ -782,14 +782,14 @@ const findQueryTarget = q => {
782
782
  return q.SELECT && q.SELECT._transitions
783
783
  ? q.SELECT._transitions[q.SELECT._transitions.length - 1].target
784
784
  : q.INSERT
785
- ? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
786
- : q.UPDATE
787
- ? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
788
- : q.UPSERT
789
- ? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
790
- : q.DELETE
791
- ? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
792
- : undefined
785
+ ? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
786
+ : q.UPDATE
787
+ ? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
788
+ : q.UPSERT
789
+ ? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
790
+ : q.DELETE
791
+ ? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
792
+ : undefined
793
793
  }
794
794
 
795
795
  module.exports = {
@@ -1282,10 +1282,10 @@ class JoinCQNFromExpanded {
1282
1282
  element.ref[0] === alias
1283
1283
  ? [...element.ref]
1284
1284
  : element.ref.length === 1
1285
- ? [alias, element.ref[0]]
1286
- : this._isPathExpressionToOne(element.ref, expandedEntity)
1287
- ? [alias, ...element.ref]
1288
- : [alias, element.ref[1]]
1285
+ ? [alias, element.ref[0]]
1286
+ : this._isPathExpressionToOne(element.ref, expandedEntity)
1287
+ ? [alias, ...element.ref]
1288
+ : [alias, element.ref[1]]
1289
1289
 
1290
1290
  return (sort && { ref, sort }) || { ref }
1291
1291
  })
@@ -97,10 +97,10 @@ class RawToExpanded {
97
97
  ? null
98
98
  : !!entry[mappings.IsActiveEntity]
99
99
  : 'IsActiveEntity' in entry
100
- ? entry.IsActiveEntity === null
101
- ? null
102
- : !!entry.IsActiveEntity
103
- : null
100
+ ? entry.IsActiveEntity === null
101
+ ? null
102
+ : !!entry.IsActiveEntity
103
+ : null
104
104
  : null
105
105
 
106
106
  // A raw row contains more elements than the config. Iterating over config is faster.
@@ -112,8 +112,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
112
112
  const _etagValidationType = req.headers['if-match']
113
113
  ? 'if-match'
114
114
  : req.headers['if-none-match']
115
- ? 'if-none-match'
116
- : undefined
115
+ ? 'if-none-match'
116
+ : undefined
117
117
 
118
118
  const query = _cleansed(req.query, this.model)
119
119
  _cleanseParams(req.params, req.target)
@@ -181,23 +181,23 @@ cds.ApplicationService.prototype.handle = async function (req) {
181
181
  const read = req.query._target.name.endsWith('.drafts')
182
182
  ? Read.ownDrafts
183
183
  : draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
184
- ? Read.all
185
- : draftParams.IsActiveEntity === true &&
186
- draftParams.SiblingEntity_IsActiveEntity === null &&
187
- (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
188
- draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
189
- ? Read.lockedByAnotherUser
190
- : draftParams.IsActiveEntity === true &&
191
- draftParams.SiblingEntity_IsActiveEntity === null &&
192
- draftParams.DraftAdministrativeData_InProcessByUser === ''
193
- ? Read.unsavedChangesByAnotherUser
194
- : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
195
- ? Read.unchanged
196
- : draftParams.IsActiveEntity === true
197
- ? Read.onlyActives
198
- : draftParams.IsActiveEntity === false
199
- ? Read.ownDrafts
200
- : Read.onlyActives
184
+ ? Read.all
185
+ : draftParams.IsActiveEntity === true &&
186
+ draftParams.SiblingEntity_IsActiveEntity === null &&
187
+ (draftParams.DraftAdministrativeData_InProcessByUser === 'not null' ||
188
+ draftParams.DraftAdministrativeData_InProcessByUser === 'not ')
189
+ ? Read.lockedByAnotherUser
190
+ : draftParams.IsActiveEntity === true &&
191
+ draftParams.SiblingEntity_IsActiveEntity === null &&
192
+ draftParams.DraftAdministrativeData_InProcessByUser === ''
193
+ ? Read.unsavedChangesByAnotherUser
194
+ : draftParams.IsActiveEntity === true && draftParams.HasDraftEntity === false
195
+ ? Read.unchanged
196
+ : draftParams.IsActiveEntity === true
197
+ ? Read.onlyActives
198
+ : draftParams.IsActiveEntity === false
199
+ ? Read.ownDrafts
200
+ : Read.onlyActives
201
201
  const result = await read(run, query)
202
202
  return result
203
203
  }
@@ -218,18 +218,27 @@ cds.ApplicationService.prototype.handle = async function (req) {
218
218
 
219
219
  if (req.event === 'DELETE' && draftParams.IsActiveEntity) {
220
220
  const draftsRef = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
221
- const draft = await SELECT.one.from({ ref: draftsRef }).columns([
221
+ const draftQuery = SELECT.one.from({ ref: draftsRef }).columns([
222
222
  { ref: ['DraftAdministrativeData_DraftUUID'] },
223
223
  {
224
224
  ref: ['DraftAdministrativeData'],
225
225
  expand: [_inProcessByUserXpr(_lock.shiftedNow)]
226
226
  }
227
227
  ])
228
+
229
+ // Deletion of active instance outside draft tree, no need to check for draft
230
+ if (!draftQuery.target?.isDraft) {
231
+ await run(query)
232
+ return req.data
233
+ }
234
+
235
+ // Deletion of active instance inside draft tree, need to check that no draft exists
236
+ const draft = await draftQuery
228
237
  const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
229
238
  if (inProcessByUser && inProcessByUser !== cds.context.user.id)
230
239
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [inProcessByUser])
231
240
  if (draft) req.reject(403, 'DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS')
232
- await run(DELETE.from({ ref: query.DELETE.from.ref }))
241
+ await run(query)
233
242
  return req.data
234
243
  }
235
244
 
@@ -387,8 +387,8 @@ const getReqOptions = (req, query, service) => {
387
387
  typeof query === 'object'
388
388
  ? _cqnToReqOptions(query, service, req)
389
389
  : typeof query === 'string'
390
- ? _stringToReqOptions(query, req.data, req.target)
391
- : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
390
+ ? _stringToReqOptions(query, req.data, req.target)
391
+ : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
392
392
 
393
393
  if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
394
394
  req.reject(501, 'Lambda expressions are not supported in OData v2')
@@ -173,14 +173,15 @@ function _processWhere(where, entity) {
173
173
  function _convertVal(element, value) {
174
174
  if (value === null) return value
175
175
  switch (element._type) {
176
- case 'cds.Integer':
177
176
  case 'cds.UInt8':
177
+ case 'cds.Integer':
178
178
  case 'cds.Int16':
179
179
  case 'cds.Int32':
180
- if (!/^\d+$/.test(value)) throw new Error('Not a valid integer')
180
+ if (!/^-?\d+$/.test(value)) throw new Error('Not a valid integer')
181
181
  // eslint-disable-next-line no-case-declarations
182
182
  const n = Number(value)
183
183
  if (!Number.isSafeInteger(n)) throw new Error('Not a valid integer')
184
+ if (element._type === 'cds.UInt8' && n < 0) throw new Error('Not a positive integer')
184
185
  return n
185
186
 
186
187
  case 'cds.String':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.4.0",
3
+ "version": "7.4.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [