@sap/cds 7.6.3 → 7.7.0

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 (94) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +18 -12
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +1 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/Query.js +1 -1
  21. package/lib/ql/SELECT.js +8 -8
  22. package/lib/req/context.js +22 -13
  23. package/lib/req/request.js +10 -4
  24. package/lib/srv/cds-connect.js +9 -3
  25. package/lib/srv/cds-serve.js +5 -3
  26. package/lib/srv/middlewares/ctx-model.js +1 -1
  27. package/lib/srv/protocols/odata-v4.js +38 -9
  28. package/lib/srv/srv-api.js +98 -140
  29. package/lib/srv/srv-models.js +2 -2
  30. package/lib/srv/srv-tx.js +1 -0
  31. package/lib/utils/cds-utils.js +32 -23
  32. package/lib/utils/data.js +1 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +4 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  42. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  43. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  44. package/libx/_runtime/cds-services/util/assert.js +50 -240
  45. package/libx/_runtime/cds.js +5 -0
  46. package/libx/_runtime/common/aspects/any.js +53 -45
  47. package/libx/_runtime/common/generic/input.js +14 -10
  48. package/libx/_runtime/common/generic/paging.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  50. package/libx/_runtime/common/utils/keys.js +1 -1
  51. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  52. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  53. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  54. package/libx/_runtime/common/utils/stream.js +2 -16
  55. package/libx/_runtime/common/utils/streamProp.js +16 -6
  56. package/libx/_runtime/common/utils/ucsn.js +1 -0
  57. package/libx/_runtime/db/utils/columns.js +6 -1
  58. package/libx/_runtime/fiori/generic/activate.js +11 -3
  59. package/libx/_runtime/fiori/generic/edit.js +8 -2
  60. package/libx/_runtime/fiori/lean-draft.js +99 -30
  61. package/libx/_runtime/hana/execute.js +2 -5
  62. package/libx/_runtime/messaging/service.js +6 -2
  63. package/libx/common/assert/index.js +232 -0
  64. package/libx/common/assert/type.js +109 -0
  65. package/libx/common/assert/utils.js +125 -0
  66. package/libx/common/assert/validation.js +109 -0
  67. package/libx/odata/index.js +5 -5
  68. package/libx/odata/middleware/create.js +83 -0
  69. package/libx/odata/middleware/delete.js +38 -0
  70. package/libx/odata/middleware/error.js +8 -0
  71. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  72. package/libx/odata/middleware/operation.js +78 -0
  73. package/libx/odata/middleware/parse.js +11 -0
  74. package/libx/odata/{read.js → middleware/read.js} +42 -20
  75. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  76. package/libx/odata/middleware/stream.js +237 -0
  77. package/libx/odata/middleware/update.js +165 -0
  78. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  79. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  80. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  81. package/libx/odata/{utils.js → utils/index.js} +91 -9
  82. package/libx/outbox/index.js +5 -4
  83. package/libx/rest/RestAdapter.js +0 -1
  84. package/libx/rest/middleware/operation.js +6 -4
  85. package/libx/rest/middleware/parse.js +20 -2
  86. package/package.json +1 -1
  87. package/server.js +43 -71
  88. package/libx/odata/create.js +0 -44
  89. package/libx/odata/delete.js +0 -25
  90. package/libx/odata/error.js +0 -12
  91. package/libx/odata/update.js +0 -110
  92. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  93. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  94. /package/libx/odata/{result.js → utils/result.js} +0 -0
package/server.js CHANGED
@@ -1,119 +1,91 @@
1
- const cds = require('./lib'), { features } = cds.env
2
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
3
- const express = require('express'), fs = require('fs'), path = require('path')
4
-
1
+ const express = require('express')// eslint-disable-line cds/no-missing-dependencies
2
+ const cds = require('./lib')
5
3
 
6
4
  /**
7
5
  * Standard express.js bootstrapping, constructing an express `application`
8
6
  * and launching a corresponding http server using `app.listen()`.
9
- * Project-specific `./server.js` can overload this and react to these
10
- * events:
11
- *
12
- * - cds.on('bootstrap',(app)) - emitted before any middleware is added
13
- * - cds.on('loaded',(model)) - emitted when a model was loaded
14
- * - cds.on('connect',(srv)) - emitted when a service was connected
15
- * - cds.on('serving',(srv)) - emitted when a service was served
16
- * - cds.on('listening',({server,url})) - emitted when the server is listening
17
- *
18
7
  * @param {object} options - canonicalized options from `cds serve` cli
19
- * @param {boolean} options.in_memory - true if we need to bootstrap an in-memory database
20
8
  * @param {string} options.service - name of service to be served; default: 'all'
21
9
  * @param {string} options.from - filenames of models to load; default: '*'
22
- * @param {express.Application} options.app - filenames of models to load; default: '*'
23
- * @param {express.Handler} options.index - custom handler for /
24
- * @param {express.Handler} options.favicon - custom handler for /favicon.ico
25
- * @returns Promise resolving to a Node.js http server as returned by express' `app.listen()`.
10
+ * @param {boolean} options.in_memory - true if we need to bootstrap an in-memory database
11
+ * @param {express.Handler} options.favicon - handler for /favicon.ico requests
12
+ * @param {express.Handler} options.index - handler for generated /index.html
13
+ * @param {express.Application} options.app - optional pre-constructed express app
14
+ * @returns {Promise<import('http').Server>} A Promise resolving to Node.js http server as returned by express' `app.listen()`.
26
15
  */
27
16
  module.exports = async function cds_server (options) {
28
17
 
29
- const _in_prod = process.env.NODE_ENV === 'production'
18
+ // prepare express app
30
19
  const o = { ...options, __proto__:defaults }
31
-
32
20
  const app = cds.app = o.app || express()
33
- app.serve = _app_serve //> app.serve allows delegating to sub modules
34
- cds.emit ('bootstrap',app) //> hook for project-local server.js
21
+ cds.emit ('bootstrap', app)
35
22
 
36
- // mount static resources and logger middleware
37
- if (o.cors) !_in_prod && app.use (o.cors) //> CORS
38
- if (o.static) app.use (express_static (o.static)) //> defaults to ./app
23
+ // mount static resources and cors middleware
24
+ if (o.cors) app.use (o.cors) //> if not in prod
25
+ if (o.static) app.use (express.static (o.static)) //> defaults to ./app
39
26
  if (o.favicon) app.use ('/favicon.ico', o.favicon) //> if none in ./app
40
27
  if (o.index) app.get ('/',o.index) //> if none in ./app
41
28
 
42
- // load specified models or all in project
43
- const csn = await cds.load(o.from||'*',o) .then (cds.minify) //> separate csn for _init_db
44
- cds.model = cds.compile.for.nodejs(csn)
29
+ // load and prepare models
30
+ const csn = await cds.load(o.from||'*',o) .then (cds.minify)
31
+ cds.model = cds.compile.for.nodejs (csn)
45
32
 
46
- // connect to essential framework services if required
33
+ // connect to essential framework services
47
34
  if (cds.requires.db) cds.db = await cds.connect.to ('db') .then (_init)
48
35
  if (cds.requires.messaging) await cds.connect.to ('messaging')
49
36
 
50
37
  // serve all services declared in models
51
38
  await cds.serve (o.service,o) .in (app)
52
- await cds.emit ('served', cds.services) //> hook for listeners
39
+ await cds.emit ('served', cds.services)
53
40
 
54
41
  // start http server
55
- const port = (o.port !== undefined) ? o.port : (process.env.PORT || cds.env.server?.port || 4004)
42
+ const port = o.port !== undefined ? o.port : ( process.env.PORT || cds.env.server?.port || 4004 )
56
43
  return app.server = app.listen (port)
57
44
 
58
- // bootstrap in-memory db
45
+ // cds.deploy in-memory db, if enabled
59
46
  async function _init (db) {
60
47
  if (!o.in_memory || cds.requires.multitenancy) return db
61
- const fts = cds.requires.toggles && cds.resolve (features.folders)
48
+ const fts = cds.requires.toggles && cds.resolve (cds.env.features.folders)
62
49
  const m = !fts ? csn : await cds.load([o.from||'*',...fts],o) .then (cds.minify)
63
50
  return cds.deploy(m).to(db,o)
64
51
  }
65
-
66
52
  }
67
53
 
68
54
 
69
- // -------------------------------------------------------------------------
70
- // Default handlers, which can be overidden by options passed to the server
71
- //
55
+ /**
56
+ * Default options, which can be overidden by options passed to cds.server().
57
+ */
72
58
  const defaults = {
73
-
74
- cors,
75
-
76
59
  get static() {
77
- const dir = cds.env.folders.app //> defaults to ./app
78
- if (dir && !fs.existsSync(path.resolve(cds.root, dir))) return undefined
79
- return dir
60
+ return cds.utils.isdir (cds.env.folders.app)
80
61
  },
81
-
82
- // default generic index.html page
83
62
  get index() {
84
63
  const index = require ('./app/index.js')
85
64
  return (_,res) => res.send (index.html)
86
65
  },
87
-
88
- // default favicon
89
66
  get favicon() {
90
67
  const favicon = require.resolve ('./app/favicon.ico')
91
68
  return express.static (favicon, {maxAge:'14d'})
69
+ },
70
+ get cors() {
71
+ return process.env.NODE_ENV === 'production' ? null : (req, res, next) => {
72
+ const { origin } = req.headers
73
+ if (origin) {
74
+ res.set('access-control-allow-origin', origin)
75
+ if (req.method === 'OPTIONS') return res.set('access-control-allow-methods', 'GET,HEAD,PUT,PATCH,POST,DELETE').end()
76
+ }
77
+ next()
78
+ }
92
79
  }
93
80
  }
94
81
 
95
82
 
96
- // Helpers to delegate to imported UIs
97
- const _app_serve = function (endpoint) { return {
98
- from: (pkg,folder) => {
99
- folder = !folder ? pkg : path.resolve(require.resolve(pkg+'/package.json',{paths:[cds.root]}),'../'+folder)
100
- this.use (endpoint, express.static(folder))
101
- if (!endpoint.endsWith('/webapp')) (this._app_links || (this._app_links = [])) .push (endpoint)
102
- }
103
- }}
104
-
105
- function cors (req, res, next) { // REVISIT: should that move into middlewares?
106
- const { origin } = req.headers
107
- if (origin) res.set('access-control-allow-origin', origin)
108
- if (origin && req.method === 'OPTIONS')
109
- return res.set('access-control-allow-methods', 'GET,HEAD,PUT,PATCH,POST,DELETE').end()
110
- next()
111
- }
112
-
113
- function express_static (dir) {
114
- return express.static (path.resolve (cds.root,dir))
115
- }
116
-
117
-
118
- // -------------------------------------------------------------------------
119
- if (!module.parent) module.exports ({from:process.argv[2]})
83
+ /**
84
+ * Helper to delegate to imported UIs. Usage:
85
+ * @example app.serve('/bookshop').from('@capire/bookshop','app/vue')
86
+ */
87
+ express.application.serve = function (endpoint) { return { from: (pkg,folder) => {
88
+ folder = !folder ? pkg : cds.utils.path.resolve (require.resolve(pkg+'/package.json',{paths:[cds.root]}),'..',folder)
89
+ this.use (endpoint, express.static(folder))
90
+ if (!endpoint.endsWith('/webapp')) (this._app_links ??= []) .push (endpoint)
91
+ }}}
@@ -1,44 +0,0 @@
1
- const cds = require('../../')
2
- const { odataError } = require('./utils')
3
- const { INSERT } = require('../../lib/ql/cds-ql')
4
- const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
5
- const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
6
- const { toODataResult } = require('./result')
7
-
8
- module.exports = srv =>
9
- function create(req, res, next) {
10
- const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
11
-
12
- const {
13
- SELECT: { one }
14
- } = query
15
-
16
- if (one) {
17
- const singleton = query.target._isSingleton
18
- const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
19
- return res.status(405).json(error)
20
- }
21
-
22
- const queryPathXpr = query.SELECT?.from
23
-
24
- const insertQuery = INSERT.into(queryPathXpr).entries(req.body)
25
-
26
- const cdsReq = new cds.Request({ query: insertQuery })
27
- return srv
28
- .dispatch(cdsReq)
29
- .then(async result => {
30
- if (cdsReq._.readAfterWrite) {
31
- // TODO see if in old odata impl for other checks that should happen
32
- result = await readAfterWrite(cdsReq, srv, { operation: { result } })
33
- }
34
-
35
- if (result == null) {
36
- res.status(204)
37
- }
38
-
39
- const info = metaInfo(insertQuery, 'CREATE', srv, req.body, req, false)
40
-
41
- return res.status(201).send(toODataResult(result, info))
42
- })
43
- .catch(next)
44
- }
@@ -1,25 +0,0 @@
1
- const cds = require('../../')
2
- const { odataError } = require('./utils')
3
-
4
- module.exports = srv =>
5
- function deleete(req, res, next) {
6
- const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
7
-
8
- let {
9
- SELECT: { one }
10
- } = query
11
-
12
- if (!one) {
13
- return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
14
- }
15
-
16
- const _target = query.SELECT && query.SELECT.from
17
- const deleteQuery = DELETE.from(_target)
18
-
19
- return srv
20
- .run(deleteQuery)
21
- .then(() => {
22
- return res.send(204)
23
- })
24
- .catch(next)
25
- }
@@ -1,12 +0,0 @@
1
- const { normalizeError } = require('../_runtime/common/error/frontend')
2
- const { odataError } = require('./utils')
3
-
4
- module.exports = _srv => (err, req, res, _next) => {
5
- const { error, statusCode } = normalizeError(err, req)
6
-
7
- if (statusCode >= 400 && statusCode < 500) {
8
- return res.status(statusCode).json(odataError(`${err.code}`, error.message))
9
- }
10
-
11
- return res.status(500).send('Internal Server Error')
12
- }
@@ -1,110 +0,0 @@
1
- const cds = require('../../')
2
- const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
3
- const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
4
- const { where2obj } = require('../_runtime/common/utils/cqn')
5
- const { toODataResult } = require('./result')
6
- const { odataError } = require('./utils')
7
-
8
- const _isUpsertAllowed = ({ target, data, event }) => {
9
- return (
10
- !(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
11
- !(target && target._isDraftEnabled && (!cds.env.fiori.lean_draft || (!data.IsActiveEntity && event === 'PATCH')))
12
- )
13
- }
14
-
15
- const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
16
- // keys not in data
17
- if (keys && Object.keys(keys).some(key => key in data)) {
18
- return false
19
- }
20
-
21
- const nav = pathExpression.ref && pathExpression.ref.length !== 0 && pathExpression.ref[1]
22
- const parent = pathExpression.ref && pathExpression.ref[0].id
23
-
24
- // not a navigation
25
- if (!parent || !nav) {
26
- return false
27
- }
28
-
29
- const navID = typeof nav === 'string' ? nav : nav.id
30
- const navElement = model.definitions[parent].elements[navID]
31
-
32
- // not a containment
33
- if (!navElement._isContained) {
34
- return false
35
- }
36
-
37
- const where = pathExpression.ref[0].where
38
- return parent && navElement && where
39
- }
40
-
41
- module.exports = srv =>
42
- function update(req, res, next) {
43
- const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
44
-
45
- const {
46
- SELECT: { one }
47
- } = query
48
-
49
- if (!one) {
50
- return res.status(405).json(odataError('405', `Method ${req.method} not allowed for ENTITY.COLLECTION`))
51
- }
52
-
53
- const queryPathXpr = query.SELECT && query.SELECT.from
54
-
55
- const isPrimitive = query._propertyAccess
56
- const data = isPrimitive ? { [query._propertyAccess]: req.body.value } : req.body
57
-
58
- const updateQuery = UPDATE.entity(queryPathXpr).with(data)
59
-
60
- // we need the cds request, so we can access req._.readAfterWrite
61
- const cdsReq = new cds.Request({ query: updateQuery })
62
-
63
- const info = metaInfo(query, 'UPDATE', srv, data, req, false)
64
-
65
- if (!isPrimitive && queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where) {
66
- Object.assign(data, where2obj(queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where))
67
- }
68
-
69
- return srv
70
- .dispatch(cdsReq)
71
- .then(async result => {
72
- if (!isPrimitive && cdsReq._.readAfterWrite) {
73
- // TODO see if in old odata impl for other checks that should happen
74
- result = await readAfterWrite(cdsReq, srv, { operation: { result } })
75
- }
76
-
77
- if (result == null) {
78
- res.status(204)
79
- }
80
-
81
- return res.send(toODataResult(result, info))
82
- })
83
- .catch(async e => {
84
- // UPSERT
85
- const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
86
- if (is404 && !isPrimitive && _isUpsertAllowed({ target: query.target, data: req.body, event: req.method })) {
87
- // PUT / PATCH with if-match header means "only if already exists", i.e., no insert if not
88
- if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
89
-
90
- if (_isNavigationWithKeyInParent(query.target.keys, data, queryPathXpr, srv.model)) {
91
- // REVISIT better error message
92
- return res.status(422).json(odataError('422', `Unprocessable Entity`))
93
- }
94
-
95
- // REVISIT: up_XX needs to be looked up -> composition of aspect
96
- const insertQuery = INSERT.into(queryPathXpr).entries(data)
97
- const cdsReq = new cds.Request({ query: insertQuery })
98
- let result = await srv.dispatch(cdsReq)
99
-
100
- if (cdsReq._.readAfterWrite) {
101
- // TODO see if in old odata impl for other checks that should happen
102
- result = await readAfterWrite(cdsReq, srv, { operation: { result } })
103
- }
104
-
105
- return res.status(201).send(toODataResult(result, info))
106
- }
107
- throw e
108
- })
109
- .catch(next)
110
- }
File without changes
File without changes