Socket
Socket
Sign inDemoInstall

@sap/cds-services

Package Overview
Dependencies
Maintainers
3
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-services - npm Package Compare versions

Comparing version 1.11.1 to 1.14.0

lib/adapter/odata-v4/utils/stream.js

49

CHANGELOG.md

@@ -9,2 +9,51 @@ # Changelog

## Version 1.14.0 - 2019-06-24
### Added
- Alternative mock strategy config
- Support for value ranges annotations for REST adapter
- Multiple authentication strategies
### Changed
- Handling of deep insert / update for associations
- Use `@sap/odata-server@1.3.4`
### Fixed
- Bound actions for draft-enabled entities
- Combination of `$apply` with other query parameters
### Removed
- Caching of metadata as odata already does it
## Version 1.13.0 - 2019-06-07
### Added
- Method `diff` to calculate the actual changes in a `CUD`request or while saving a draft
- Support authorization annotations for actions and functions
- Support for default sort order using `@cds.default.order` or `@odata.default.order`
- Support for writing binary stream through odata
## Version 1.12.0 - 2019-05-24
### Added
- Support for localized in generic handlers (no compositions / associations)
- Handler registration by path for autoexposed and redirected entities
- Support for Rest parametric functions and actions
### Changed
- Renamed `Service.with` to `Service.impl`
### Fixed
- `falsy` values as default value
- `req.info` in case of draft actions
- Scopes are checked before custom before handlers
## Version 1.11.1 - 2019-05-16

@@ -11,0 +60,0 @@

8

lib/adapter/auth/Mock.js

@@ -70,3 +70,3 @@ const DEFAULTS = {

return this._users[credentials.userId].jwt || {}
return this._users[credentials.userId].jwt || this._users[credentials.userId]
}

@@ -165,6 +165,6 @@

return {
userAttributes: context['xs.user.attributes'],
scopes: context.scope,
userAttributes: context['xs.user.attributes'] || context.attributes,
scopes: context.scope || context.roles,
userInfo: {
logonName: context.user_name,
logonName: context.user_name || context.ID,
givenName: context.given_name,

@@ -171,0 +171,0 @@ familyName: context.family_name,

@@ -5,3 +5,3 @@ const { FeatureNotSupported, getError } = require('../../errors')

const _getBasicAuthHandler = credentials => {
return (user, password, done) => {
return function (user, password, done) {
if (credentials[user] === password) {

@@ -11,3 +11,4 @@ return done(null, { id: user })

return done(getError(401))
// use this.fail() instead of done bacause of multiple strategies
return this.fail(getError(401))
}

@@ -43,14 +44,37 @@ }

const _getStrategy = options => {
if (typeof options.passport.strategy === 'object' && options.passport.strategy.authenticate) {
return options.passport.strategy
const getStrategyByName = (strategy, options, iterator) => {
switch (strategy) {
case 'basic':
return _basic(
Array.isArray(options.passport.credentials)
? options.passport.credentials[iterator.credentials++]
: options.passport.credentials
)
case 'JWT':
return _jwt(Array.isArray(options.uaa) ? options.uaa[iterator.uaa++] : options.uaa)
case 'mock':
return _mock(
Array.isArray(options.passport.users) ? options.passport.users[iterator.users++] : options.passport.users
)
case 'dummy':
return false
}
setImmediate(() => {
throw new FeatureNotSupported(`Authentication strategy "${options.passport.strategy}" is not supported`)
})
}
const _getOneStrategyByName = options => {
switch (options.passport.strategy) {
case 'basic':
return _basic(options.passport.credentials)
return Array.isArray(options.passport.credentials)
? options.passport.credentials.map(crd => _basic(crd))
: [_basic(options.passport.credentials)]
case 'JWT':
return _jwt(options.uaa)
return Array.isArray(options.uaa) ? options.uaa.map(uaa => _jwt(uaa)) : [_jwt(options.uaa)]
case 'mock':
return _mock(options.passport.users)
return Array.isArray(options.passport.users)
? options.passport.users.map(usr => _mock(usr))
: [_mock(options.passport.users)]
case 'dummy':

@@ -65,2 +89,27 @@ return false

const _getStrategy = options => {
if (typeof options.passport.strategy === 'object' && options.passport.strategy.authenticate) {
return [options.passport.strategy]
}
if (Array.isArray(options.passport.strategy)) {
const iterator = {
credentials: 0,
uaa: 0,
users: 0
}
const strategies = []
for (const stg of options.passport.strategy) {
if (stg.authenticate) {
strategies.push(stg)
} else {
strategies.push(getStrategyByName(stg, options, iterator))
}
}
return strategies
}
return _getOneStrategyByName(options)
}
/**

@@ -103,11 +152,21 @@ * In case there are security annotions and xssec is installed, auto configuration.

const _getPassport = (options, model, serviceName) => {
const strategy = options.passport ? _getStrategy(options) : _autoDetectStrategy(options, model, serviceName)
const strategies = options.passport ? _getStrategy(options) : _autoDetectStrategy(options, model, serviceName)
if (!strategy) {
return strategy
if (!strategies) {
return strategies
}
if (strategies.length === 0) {
return undefined
}
const passport = require('passport')
passport.use(strategy)
return passport
const names = []
for (let i = 0; i < strategies.length; i++) {
const name = `${serviceName}_${i}`
names.push(name)
passport.use(name, strategies[i])
}
return { passport, names }
}

@@ -132,18 +191,11 @@

const _strategyName = passport => {
return passport.strategy.name || passport.strategy
}
const passport = (service, app, auditLogger, options) => {
const passport = _getPassport(options, service.model, service.name)
const pass = _getPassport(options, service.model, service.name)
if (passport) {
app.use(service.path, passport.initialize())
app.use(
service.path,
require('./passportAuthenticateCallback')(passport, _strategyName(options.passport), auditLogger)
)
if (typeof pass === 'object') {
app.use(service.path, pass.passport.initialize())
app.use(service.path, require('./passportAuthenticateCallback')(pass.passport, pass.names, auditLogger))
app.use(service.path, require('./serviceAuth')(service.model.definitions[service.name], auditLogger))
// Security annotations, but no passport
} else if (passport === false) {
} else if (pass === false) {
app.use(service.path, require('./serviceAuth')(service.model.definitions[service.name], auditLogger))

@@ -150,0 +202,0 @@ }

@@ -28,6 +28,6 @@ const cds = require('../../cds')

const csn = await cds.mtx.getCsn(tenantId)
const service = serviceFactory(csn, this._odata.options)
const service = serviceFactory(csn, this._odata._options)
service._isExtended = true
const edm = cds.compile.to.edm(csn, { service: service.options.service, version: 'v4' })
const odata = new OData(edm, this._odata.options)
const odata = new OData(edm, this._odata._options)
odata.addCDSServiceToChannel(service)

@@ -34,0 +34,0 @@

@@ -5,4 +5,5 @@ const {

const getContextObject = require('../utils/context-object')
const setSapMessageHeader = require('../utils/sap-message-header')
const { toODataResult } = require('../utils/event')
const { validateResourcePathLength } = require('../utils/request')
const { validateResourcePath } = require('../utils/request')

@@ -77,6 +78,6 @@ const _getLastSegment = req => {

// End here if length is greater then allowed
validateResourcePathLength(req, options)
validateResourcePath(req, options, service.model)
const lastSegment = _getLastSegment(req)
const noProxyTarget = lastSegment === 'draftEdit'
const noProxyTarget = !['draftPrepare', 'draftActivate'].includes(lastSegment)
const context = getContextObject(ACTION_EXECUTE_HANDLER, service, req, res, noProxyTarget)

@@ -87,2 +88,3 @@

.then(result => {
setSapMessageHeader(res, context._.infos)
context.emit('succeeded')

@@ -94,2 +96,3 @@ context.emit('done')

context.emit('failed', err)
setSapMessageHeader(res, context._.infos)
context.emit('done')

@@ -96,0 +99,0 @@ next(err)

@@ -7,3 +7,3 @@ const {

const { toODataResult } = require('../utils/event')
const { validateResourcePathLength } = require('../utils/request')
const { validateResourcePath } = require('../utils/request')

@@ -19,3 +19,3 @@ /**

// End here if length is greater then allowed
validateResourcePathLength(req, options)
validateResourcePath(req, options, service.model)

@@ -22,0 +22,0 @@ const context = getContextObject(DATA_CREATE_HANDLER, service, req, res)

@@ -6,3 +6,3 @@ const {

const setSapMessageHeader = require('../utils/sap-message-header')
const { validateResourcePathLength } = require('../utils/request')
const { validateResourcePath } = require('../utils/request')

@@ -18,3 +18,3 @@ /**

// End here if length is greater then allowed
validateResourcePathLength(req, options)
validateResourcePath(req, options, service.model)

@@ -21,0 +21,0 @@ const context = getContextObject(DATA_DELETE_HANDLER, service, req, res)

@@ -19,6 +19,4 @@ const cds = require('../../../cds')

try {
if (!service._edmx) {
service._edmx = await cds.mtx.getEdmx(tenantId, service.name, locale)
}
return next(null, toODataResult(service._edmx))
const edmx = await cds.mtx.getEdmx(tenantId, service.name, locale)
return next(null, toODataResult(edmx))
} catch (err) {

@@ -29,7 +27,7 @@ return next({ statusCode: 500 })

if (!service._edmx) {
service._edmx = cds.compile.to.edmx(service._csn, Object.assign({ version: 'v4' }, options))
}
const localized = cds.localize(service._csn, locale, service._edmx)
const localized = cds.localize(
service._csn,
locale,
cds.compile.to.edmx(service._csn, Object.assign({ version: 'v4' }, options))
)
return next(null, toODataResult(localized))

@@ -36,0 +34,0 @@ }

@@ -16,5 +16,6 @@ const {

const { toODataResult } = require('../utils/event')
const { validateResourcePathLength } = require('../utils/request')
const { validateResourcePath } = require('../utils/request')
const EventEmitter = require('events')
const DelayedEmitter = require('../../utils/DelayedEmitter')
const { isStreaming } = require('../utils/stream')

@@ -147,13 +148,2 @@ /**

const _isStreaming = segments => {
const lastSegment = segments[segments.length - 1]
return (
lastSegment.getKind() === 'PRIMITIVE.PROPERTY' &&
lastSegment
.getProperty()
.getType()
.getName() === 'Stream'
)
}
/**

@@ -326,3 +316,3 @@ * Checks whether the count needs to be included in the result set as an annotation.

if (_isStreaming(segments)) {
if (isStreaming(segments)) {
return _readStream(service, context, segments)

@@ -360,3 +350,3 @@ }

// End here if length is greater then allowed
validateResourcePathLength(req, options)
validateResourcePath(req, options, service.model)

@@ -363,0 +353,0 @@ const context = getContextObject(DATA_READ_HANDLER, service, req, res, true)

@@ -7,3 +7,3 @@ const {

const { toODataResult } = require('../utils/event')
const { validateResourcePathLength } = require('../utils/request')
const { validateResourcePath } = require('../utils/request')

@@ -23,3 +23,3 @@ /**

// End here if length is greater then allowed
validateResourcePathLength(req, options)
validateResourcePath(req, options, service.model)
// TODO: Measure ODataIn, also in other handlers

@@ -26,0 +26,0 @@ const context = getContextObject(DATA_UPDATE_HANDLER, service, req, res)

@@ -126,3 +126,3 @@ const ql = require('@sap/cds-ql')

*/
const _getInnerExpandItems = (maxExpandSize, expandItem, targetType) => {
const _getInnerExpandItems = (reflectedEntity, maxExpandSize, expandItem, targetType) => {
if (!expandItem || !expandItem.getOption(QueryOptions.EXPAND)) {

@@ -132,3 +132,3 @@ return []

return expandToCQN(maxExpandSize, expandItem.getOption(QueryOptions.EXPAND), targetType)
return expandToCQN(reflectedEntity, maxExpandSize, expandItem.getOption(QueryOptions.EXPAND), targetType)
}

@@ -157,6 +157,7 @@

const _getItemCQN = (maxExpandSize, name, navigationProperty, expandItem) => {
const _getItemCQN = (reflectedEntity, maxExpandSize, name, navigationProperty, expandItem) => {
_notSupported(expandItem)
const targetType = navigationProperty.getEntityType()
const relatedEntity = reflectedEntity.elements[name]._target
const item = {

@@ -166,4 +167,5 @@ ref: [name],

}
item.expand.push(..._getInnerExpandItems(maxExpandSize, expandItem, targetType))
item.expand.push(..._getInnerExpandItems(relatedEntity, maxExpandSize, expandItem, targetType))
if (!expandItem) {

@@ -174,3 +176,3 @@ _limit(item, maxExpandSize, 0, maxExpandSize)

orderByToCQN(item, expandItem.getOption(QueryOptions.ORDERBY))
orderByToCQN(relatedEntity, item, expandItem.getOption(QueryOptions.ORDERBY))
_limit(

@@ -193,3 +195,3 @@ item,

*/
const expandToCQN = (maxExpandSize, expandItems, type) => {
const expandToCQN = (reflectedEntity, maxExpandSize, expandItems, type) => {
const allElements = []

@@ -202,3 +204,3 @@ const isAll = _isAll(expandItems)

if (isAll || expandItem) {
allElements.push(_getItemCQN(maxExpandSize, name, navigationProperty, expandItem))
allElements.push(_getItemCQN(reflectedEntity, maxExpandSize, name, navigationProperty, expandItem))
}

@@ -205,0 +207,0 @@ }

@@ -35,13 +35,17 @@ const ExpressionToCQN = require('./ExpressionToCQN')

const orderbyToCQN = (cqnPartial, orderBy) => {
if (!orderBy || orderBy.length === 0) {
return
const orderbyToCQN = (reflectedEntity, cqnPartial, orderBy) => {
if (orderBy) {
cqnPartial.orderBy = cqnPartial.orderBy || []
for (const order of orderBy) {
cqnPartial.orderBy.push(_orderExpression(order))
}
}
if (!cqnPartial.orderBy) {
cqnPartial.orderBy = []
}
const defaultOrders = reflectedEntity['@cds.default.order'] || reflectedEntity['@odata.default.order']
for (const order of orderBy) {
cqnPartial.orderBy.push(_orderExpression(order))
if (defaultOrders) {
cqnPartial.orderBy = cqnPartial.orderBy || []
for (const defaultOrder of defaultOrders) {
cqnPartial.orderBy.push({ ref: [defaultOrder.by['=']], sort: defaultOrder.desc ? 'desc' : 'asc' })
}
}

@@ -48,0 +52,0 @@ }

@@ -75,6 +75,4 @@ const QueryOptions = require('@sap/odata-server').QueryOptions

const _orderby = (uriInfo, queryOptions, cqn) => {
if (queryOptions) {
orderByToCQN(cqn.SELECT, uriInfo.getQueryOption(QueryOptions.ORDERBY))
}
const _orderby = (reflectedEntity, uriInfo, cqn) => {
orderByToCQN(reflectedEntity, cqn.SELECT, uriInfo.getQueryOption(QueryOptions.ORDERBY))
}

@@ -130,3 +128,3 @@

const _expand = (maxExpandSize, uriInfo) => {
const _expand = (reflectedEntity, maxExpandSize, uriInfo) => {
const expand = uriInfo.getQueryOption(QueryOptions.EXPAND)

@@ -138,3 +136,3 @@

return expandToCQN(maxExpandSize, expand, uriInfo.getFinalEdmType())
return expandToCQN(reflectedEntity, maxExpandSize, expand, uriInfo.getFinalEdmType())
}

@@ -164,2 +162,18 @@

const _cleanupForApply = (apply, cqn) => {
if (Object.keys(apply).length !== 0) {
// cleanup order by columns which are not part of columns
const selectColumns = cqn.SELECT.columns.map(c => c.as || c.ref[c.ref.length - 1])
if (cqn.SELECT.orderBy) {
const newOrderBy = cqn.SELECT.orderBy.filter(o => o.ref && selectColumns.includes(o.ref[o.ref.length - 1]))
// remove path expressions because subselect only uses columns
cqn.SELECT.orderBy = newOrderBy.map(col => ({ ref: [col.ref[col.ref.length - 1]], sort: col.sort }))
}
if (!cqn.SELECT.orderBy || !cqn.SELECT.orderBy.length) {
delete cqn.SELECT.orderBy
}
}
}
/**

@@ -184,3 +198,3 @@ * Transform odata READ request into a CQN object.

const select = _select(queryOptions, reflectedEntity.keys, target)
const expand = _expand(service.options.maxExpandSize, uriInfo)
const expand = _expand(reflectedEntity, service.options.maxExpandSize, uriInfo)

@@ -207,3 +221,4 @@ // TODO: Correct implementation of the combined apply, select and expand as described in

const cqn = SELECT.from(target, propertyParam.length > 0 ? propertyParam : select)
let cqn = SELECT.from(target, propertyParam.length > 0 ? propertyParam : select)
if (isNavigation(segments)) {

@@ -215,2 +230,9 @@ enhanceCqnWithSubSelects(cqn, segments, service.model, SELECT)

if (Object.keys(apply).length !== 0) {
_groupBy(apply.groupBy, cqn)
const cols = cqn.SELECT.columns.map(col => col.as || col.ref[col.ref.length - 1])
// build subselect for apply
cqn = SELECT.from(cqn, cols)
}
const kind = segments[segments.length - 1].getKind()

@@ -221,7 +243,6 @@

_search(reflectedEntity, uriInfo, cqn, queryOptions)
_groupBy(apply.groupBy, cqn)
}
if (_isCollectionOrToMany(kind)) {
_orderby(uriInfo, queryOptions, cqn)
_orderby(reflectedEntity, uriInfo, cqn)
_topSkip(queryOptions, cqn)

@@ -231,2 +252,4 @@ topSkipWithPaginationToCQN(uriInfo, cqn)

_cleanupForApply(apply, cqn)
return cqn

@@ -233,0 +256,0 @@ }

const { FeatureNotSupported } = require('../../../errors')
const { isStreaming } = require('../utils/stream')

@@ -31,5 +32,6 @@ const _removeIds = (obj, keysAndValues) => {

const segment = segments[segments.length - 1]
const streaming = isStreaming(segments)
if (SUPPORTED_KINDS.includes(segment.getKind())) {
const keysAndValues = _keysAndValues(segment)
if (SUPPORTED_KINDS.includes(segment.getKind()) || streaming) {
const keysAndValues = streaming ? _keysAndValues(segments[segments.length - 2]) : _keysAndValues(segment)

@@ -36,0 +38,0 @@ const cqn = context.statements.UPDATE(context.target).set(_removeIds(context.data, keysAndValues))

@@ -15,2 +15,3 @@ const {

const { addDefaultValuesDeep } = require('../../../util/dataProcessUtils')
const { isStreaming } = require('./stream')

@@ -99,2 +100,13 @@ const ALLOWED_INFO_PROPERTIES = ['code', 'message', 'numericSeverity', 'longtextUrl']

const _getFunctionParameters = (lastSegment, keyValues) => {
const functionParameters = lastSegment.getFunctionParameters()
const paramValues = _getParamData(functionParameters)
// Working assumption for the case of name collisions: take the entity's key
for (const key of Object.keys(keyValues)) {
paramValues[key] = keyValues[key]
}
return paramValues
}
/**

@@ -113,15 +125,10 @@ * Get data from odata-v4.

const _getData = (component, req, annotatedColumns, target) => {
const segments = req.getUriInfo().getPathSegments()
const lastSegment = req.getUriInfo().getLastSegment()
const keyPredicates = lastSegment.getKeyPredicates()
const streaming = isStreaming(segments)
const keyPredicates = streaming ? segments[segments.length - 2].getKeyPredicates() : lastSegment.getKeyPredicates()
const keyValues = _getParamData(keyPredicates)
if (component === DATA_READ_HANDLER && _isFunctionInvocation(req)) {
const functionParameters = lastSegment.getFunctionParameters()
const paramValues = _getParamData(functionParameters)
// Working assumption for the case of name collisions: take the entity's key
for (const key of Object.keys(keyValues)) {
paramValues[key] = keyValues[key]
}
return paramValues
return _getFunctionParameters(lastSegment, keyValues)
}

@@ -134,4 +141,10 @@

// Use identifier from URL instead of body
const data = req.getBody() || {}
let data = req.getBody() || {}
if (streaming && typeof data.pipe === 'function') {
const dataObj = {}
dataObj[lastSegment.getProperty().getName()] = data
data = dataObj
}
// Only to be done for post via navigation

@@ -138,0 +151,0 @@ if (component === DATA_CREATE_HANDLER && lastSegment.getKind() === 'NAVIGATION.TO.MANY') {

@@ -5,3 +5,3 @@ const {

UriResource: {
ResourceKind: { NAVIGATION_TO_MANY }
ResourceKind: { NAVIGATION_TO_MANY, ENTITY, ENTITY_COLLECTION }
}

@@ -45,12 +45,12 @@ }

return lastSegment.getKind() === NAVIGATION_TO_MANY ? lastSegment.getTarget().getMaxPageSize() : undefined
return lastSegment.getKind() === NAVIGATION_TO_MANY ? lastSegment.getTarget() && lastSegment.getTarget().getMaxPageSize() : undefined
}
/**
* Validate resource path length.
* It will throw an error in case the maximum is exceeded.
* Validate resource path length and autoexposed entities.
* It will throw an error in case the maximum is exceeded or top entity is autoexposed.
* @param {Object} req odata request
* @param {Object} options odata configuration options
*/
const validateResourcePathLength = (req, options) => {
const validateResourcePath = (req, options, model) => {
const { getError } = require('../../../errors')

@@ -61,2 +61,15 @@

}
const segment = req.getUriInfo().getPathSegments()[0]
if (segment.getKind() === ENTITY || segment.getKind() === ENTITY_COLLECTION) {
const name = segment.getEntitySet().getName()
const entity = model.definitions[`${options.service}.${name}`]
// For auto-exposed Compositions all direct CRUD requests are rejected
// For other auto-exposed entities only C_UD are rejected. Direct READ is allowed.
if (entity && entity['@cds.autoexposed']) {
if (req.getIncomingRequest().method !== 'GET' || !entity['@cds.autoexpose']) {
throw getError(400, `Entity ${name} is autoexposed`)
}
}
}
}

@@ -80,4 +93,4 @@

maxPageSize,
validateResourcePathLength,
validateResourcePath,
skipToken
}

@@ -35,2 +35,5 @@ const getContextObject = require('../utils/context-object')

const err = validationChecks(context.data, context.target || { elements: parsedUrl.segments[0].params })
if (err) return handleError(err, service, res)
if (parsedUrl.customOperation) {

@@ -54,4 +57,2 @@ const operation = parsedUrl.segments[parsedUrl.segments.length - 1]

}
const err = validationChecks(context)
if (err) return handleError(err, service, res)

@@ -58,0 +59,0 @@ return service

@@ -19,3 +19,3 @@ const getContextObject = require('../utils/context-object')

const context = getContextObject(service, parsedUrl, req, res)
const err = validationChecks(context)
const err = validationChecks(context.data, context.target)
if (err) return handleError(err, service, res)

@@ -22,0 +22,0 @@

@@ -1,2 +0,2 @@

const getKeyValuePair = require('../utils/key-value-pair')
const { getKeyValuePair } = require('../utils/key-value-utils')
const getColumns = require('../../../services/utils/columns')

@@ -34,3 +34,12 @@

if (parsedUrl.isCollection) {
return cqn.limit(..._getPaging(service, context._.req))
cqn.limit(..._getPaging(service, context._.req))
// no query option for ordering supported yet
if (parsedUrl.segments[0]['@cds.default.order']) {
for (const defaultOrder of parsedUrl.segments[0]['@cds.default.order']) {
cqn.orderBy(defaultOrder.by['='], defaultOrder.desc ? 'desc' : 'asc')
}
}
return cqn
}

@@ -37,0 +46,0 @@

const getStatements = require('../../utils/getStatements')
const restToCqn = require('../rest-to-cqn')
const getAnnotatedElements = require('../../utils/getAnnotatedElements')
const getKeyValuePair = require('./key-value-pair')
const { getKeyValuePair } = require('./key-value-utils')
const { addDefaultValuesDeep } = require('../../../util/dataProcessUtils')

@@ -93,2 +93,11 @@ const EventEmitter = require('events')

let target
// TODO: replace with generic solution, target is either the first segment (no associations) or undefined for custom operations
if (!_isCustomOperation(parsedUrl.segments[0])) {
target = parsedUrl.segments[0]
}
const errors = []
const context = {

@@ -98,11 +107,12 @@ user,

event,
errors,
get data () {
let data
if (_isCustomOperation(parsedUrl.segments[parsedUrl.segments.length - 1])) {
// TODO: if action or function we do not support parameters yet
// retrieve function/action parameters
return {}
// data = parsedUrl.params || _validatedBodyValues(req.body, parsedUrl, this) || {}
data = parsedUrl.params || req.body || {}
} else {
const annotatedColumns = _getAnnotatedColumns(parsedUrl.method, this)
data = _getData(parsedUrl, annotatedColumns, this.target, req)
}
const annotatedColumns = _getAnnotatedColumns(parsedUrl.method, this)
const data = _getData(parsedUrl, annotatedColumns, this.target, req)
Object.defineProperty(context, 'data', { value: data })

@@ -117,13 +127,3 @@ return data

},
get target () {
let target
// TODO: replace with generic solution, target is either the first segment (no associations) or undefined for custom operations
if (!_isCustomOperation(parsedUrl.segments[0])) {
target = parsedUrl.segments[0]
}
Object.defineProperty(context, 'target', { value: target })
return target
},
target,
get statements () {

@@ -130,0 +130,0 @@ const statements = getStatements(service)

const { getError } = require('../../../errors')
const { getConvertedValue } = require('./key-value-utils')
const { checkStatic } = require('../../../util/assert')

@@ -33,2 +35,23 @@ const _normalizeAndSplitUrl = req => {

const _parseEntityOrOperation = part => {
let decodedPart = decodeURI(part)
decodedPart = decodedPart.replace(/"/gi, '')
decodedPart = decodedPart.replace(/ /, '')
const [, name, paramsString = ''] = decodedPart.match(/([^(]+)\(?(.*[^)]+)?\)?/)
const params = paramsString
.split(',')
.map(keyValue => keyValue.split('='))
.reduce((obj, [key, value]) => {
if (key) {
obj[key] = value
}
return obj
}, {})
const returnObj = { name }
if (Object.keys(params).length > 0) {
returnObj.params = params
}
return returnObj
}
const _findEntityOrCustomOperation = (customOperation, service, name) => {

@@ -71,2 +94,17 @@ const thing = service.entities[name] || service.operations[name]

const _validateAndConvertParamValues = (csnElement, params = {}) => {
for (const param of Object.keys(params)) {
const csnElementParam = csnElement.params[param]
if (!csnElementParam) {
throw getError(400, `Invalid parameter: ${param}`)
}
const convertedParam = getConvertedValue(csnElementParam.type, params[param])
if (Number.isNaN(convertedParam)) {
throw getError(400, `Parameter value for '${param}' must be of type ${csnElementParam.type}`)
}
params[param] = convertedParam
}
checkStatic({ elements: csnElement.params }, params)
}
const _setConvenienceProperties = parsed => {

@@ -91,19 +129,11 @@ const lastElement = parsed.segments[parsed.segments.length - 1]

if (parts.length === 1) {
parsed.segments.push(_findEntityOrCustomOperation(customOperation, service, parts[0]))
_parseCreateOrRead1(parts, customOperation, service, parsed)
}
if (parts.length === 2) {
if (method === 'CREATE') {
throw getError(400, 'POST is only supported on resource collections or actions')
}
parsed.segments.push(_validateEntity(service.entities[parts[0]]), parts[1])
_parseCreateOrRead2(method, parsed, service, parts)
}
if (parts.length === 3) {
const entity = _validateEntity(service.entities[parts[0]])
const key = parts[1]
const operation = _validateCustomOperation(entity, parts[2], customOperation)
parsed.segments.push(entity, key, operation)
_parseCreateOrRead3(service, parts, customOperation, parsed)
}

@@ -146,1 +176,33 @@

}
const _parseCreateOrRead3 = (service, parts, customOperation, parsed) => {
const entity = _validateEntity(service.entities[parts[0]])
const key = parts[1]
const { name, params } = _parseEntityOrOperation(parts[2])
const operation = _validateCustomOperation(entity, name, customOperation)
if (params) {
_validateAndConvertParamValues(operation, params)
}
if (params && customOperation === 'function') {
parsed.params = params
}
parsed.segments.push(entity, key, operation)
}
const _parseCreateOrRead1 = (parts, customOperation, service, parsed) => {
const { name, params } = _parseEntityOrOperation(parts[0])
const entityOrCustomOperation = _findEntityOrCustomOperation(customOperation, service, name)
if (params) {
_validateAndConvertParamValues(entityOrCustomOperation, params)
}
if (params && customOperation === 'function') {
parsed.params = params
}
parsed.segments.push(entityOrCustomOperation)
}
const _parseCreateOrRead2 = (method, parsed, service, parts) => {
if (method === 'CREATE') {
throw getError(400, 'POST is only supported on resource collections or actions')
}
parsed.segments.push(_validateEntity(service.entities[parts[0]]), parts[1])
}
const { processDeep } = require('../../../util/dataProcessUtils')
const { checkStatic } = require('../../../util/assert')
const combineErrors = require('../../../errors/combineErrors')
module.exports = context => {
module.exports = (data, target) => {
const checkResult = []

@@ -9,3 +9,3 @@ const staticChecker = (entry, entity) => {

}
processDeep(staticChecker, context.data, context.target, false, true)
processDeep(staticChecker, data, target, false, true)

@@ -12,0 +12,0 @@ const error = combineErrors(checkResult)

@@ -33,25 +33,40 @@ const _addRestriction = (event, restrict, annotations) => {

const _isOperation = def => {
return ['action', 'function'].includes(def.kind)
}
const _addRestrictionsForMultipleEvents = (events, restrict, annotations) => {
for (const event of restrict.grant) {
_addRestrictions(event, restrict, annotations)
}
}
/**
* Collect entity annotations in form {EVENT: {to: [], where: []}}
* Collect authorization annotations for an entity or an action/function in form {EVENT: {to: [], where: []}}.
* For actions/functions EVENT is always set to 'operation'.
*
* @param entity
* @returns {object}
* @param {Object} def - definition of the entity or action/function
* @returns {Object}
*
*/
const getAnnotations = entity => {
const getAnnotations = def => {
const annotations = {}
const requires = entity['@requires']
if (typeof requires === 'string' || Array.isArray(requires)) {
_addRestrictions('*', { to: requires }, annotations)
if (def['@requires']) {
const requires = def['@requires']
if (_isOperation(def)) {
_addRestriction('operation', { to: requires }, annotations)
} else if (typeof requires === 'string' || Array.isArray(requires)) {
_addRestrictions('*', { to: requires }, annotations)
}
}
if (entity['@restrict']) {
for (const restrict of entity['@restrict']) {
if (typeof restrict.grant === 'string') {
if (def['@restrict']) {
for (const restrict of def['@restrict']) {
if (_isOperation(def)) {
_addRestriction('operation', restrict, annotations)
} else if (typeof restrict.grant === 'string') {
_addRestrictions(restrict.grant, restrict, annotations)
} else if (Array.isArray(restrict.grant)) {
for (const event of restrict.grant) {
_addRestrictions(event, restrict, annotations)
}
_addRestrictionsForMultipleEvents(restrict.grant, restrict, annotations)
}

@@ -58,0 +73,0 @@ }

@@ -12,3 +12,3 @@ /**

constructor (element, model) {
super(`Element ${element} is not defined in the model ${model}`)
super(`Element ${element} is not defined in the model${filename(model)}`)
this.name = this.constructor.name

@@ -19,2 +19,8 @@ Error.captureStackTrace(this, this.constructor)

const filename = model => {
if (!model) return ''
let srv = model.find('service')
return (srv && srv.$location && ` ${srv.$location.file}`) || ''
}
module.exports = NotInModel

@@ -233,3 +233,3 @@ const cds = require('../../cds')

if (!entity) {
return Promise.reject(new NotInModel(entity, this.model.find('service')['@source']))
return Promise.reject(new NotInModel(entity, this.model))
}

@@ -288,3 +288,3 @@

if (!normalizedEntity) {
return Promise.reject(new NotInModel(entity, this.model.find('service')['@source']))
return Promise.reject(new NotInModel(entity, this.model))
}

@@ -417,3 +417,3 @@

const rejected = Promise.reject(
new NotInModel(typeof entity === 'object' ? entity.name : entity, this.model.find('service')['@source'])
new NotInModel(typeof entity === 'object' ? entity.name : entity, this.model)
)

@@ -485,2 +485,33 @@

/**
* Adds properties of sharedContext to context if available
* @param {*} context - to be enhanced
* @param {*} sharedContext - source context
*/
_enhanceContextWithSharedContext (context, sharedContext) {
context._ = sharedContext._
context.user = sharedContext.user
context.attr = sharedContext.attr
if (sharedContext.run) {
context.run = sharedContext.run
}
if (sharedContext.draftMetadata) {
context.draftMetadata = sharedContext.draftMetadata
}
if (sharedContext.user) {
context.user = sharedContext.user
}
if (sharedContext.attr) {
context.attr = sharedContext.attr
}
if (sharedContext.info) {
context.info = sharedContext.info
}
}
/**
* Create a context object for the service call.

@@ -515,21 +546,3 @@ * @param event {String}

if (sharedContext) {
context._ = sharedContext._
context.user = sharedContext.user
context.attr = sharedContext.attr
if (sharedContext.run) {
context.run = sharedContext.run
}
if (sharedContext.draftMetadata) {
context.draftMetadata = sharedContext.draftMetadata
}
if (sharedContext.user) {
context.user = sharedContext.user
}
if (sharedContext.attr) {
context.attr = sharedContext.attr
}
this._enhanceContextWithSharedContext(context, sharedContext)
} else {

@@ -536,0 +549,0 @@ context._ = {}

const {
messages: { DB_CONNECTION_MISSING }
} = require('../utils/constants')
const { getSelectCQN, checkAll } = require('../utils/handlerUtils')
const { getSelectCQN, checkNotNull, filterReadOnly } = require('../utils/handlerUtils')

@@ -30,6 +30,8 @@ /**

if (checkAll(context)) {
if (checkNotNull(context)) {
return
}
filterReadOnly(context)
await context.run(context.query)

@@ -36,0 +38,0 @@

const {
messages: { DB_CONNECTION_MISSING }
messages: { DB_CONNECTION_MISSING },
DRAFT_COLUMNS
} = require('../utils/constants')

@@ -49,4 +50,2 @@ const { getEnrichedCQN, removeDraftUUID, ensureDraftsSuffix, ensureNoDraftsSuffix } = require('../utils/draftUtils')

const DRAFT_COLUMNS = ['IsActiveEntity', 'HasActiveEntity', 'HasDraftEntity', 'DraftAdministrativeData_DraftUUID']
const DRAFT_COLUMNS_CASTED = [

@@ -53,0 +52,0 @@ {

@@ -6,3 +6,3 @@ const {

const { addDefaultValuesFlat } = require('../../util/dataProcessUtils')
const { getSelectCQN, checkAll } = require('../utils/handlerUtils')
const { getSelectCQN, checkNotNull, filterReadOnly } = require('../utils/handlerUtils')

@@ -17,6 +17,6 @@ const _getInsertCQN = context => {

const _insertMissing = async context => {
if (checkAll(context)) {
if (checkNotNull(context)) {
return
}
filterReadOnly(context)
await context.run(_getInsertCQN(context))

@@ -29,2 +29,6 @@

const _containsChanges = context => {
return Object.keys(context.data).length > 1
}
/**

@@ -55,5 +59,5 @@ * Generic Handler for UPDATE requests.

context._oldData = result[0]
filterReadOnly(context)
// reject with error if query update fails (not authenticated)
if ((await context.run(context.query)) === 0) {
if (_containsChanges(context) && (await context.run(context.query)) === 0) {
context.reject(403)

@@ -60,0 +64,0 @@ return

@@ -62,2 +62,4 @@ const { EventHandlerNotDefined, NotInModel } = require('../../errors')

}
} else if (typeof entity === 'string' && entity.includes('/')) {
this.use(event, this._getPathEntity(entity.split('/')), handler)
} else {

@@ -68,2 +70,21 @@ this._add(this._normalizeEvent(event), this._objectEntityToString(entity), this._generateHandlerIfCQN(handler))

_getPathEntity (path) {
let entity = this._model.definitions[`${this._service}.${path[0]}`]
if (!entity) {
throw new NotInModel(`${this._service}.${path[0]}`, this._model)
}
for (let i = 1, len = path.length; i < len; i++) {
let assoc = entity.elements[path[i]]
if (!assoc) {
throw new NotInModel(assoc, this._model)
}
entity = assoc._target
if (!entity) {
throw new NotInModel(`${assoc}._target`, this._model)
}
}
return entity
}
_normalizeNoEntity (event, handler) {

@@ -209,3 +230,3 @@ if (this._isModeledEntity(this._objectEntityToString(Array.isArray(event) ? event[0] : event))) {

if (typeof entity !== 'string' || !this._isModeledEntity(entity)) {
throw new NotInModel(entity, this._model.find('service')['@source'])
throw new NotInModel(entity, this._model)
}

@@ -236,3 +257,3 @@ }

if (entity === undefined && !this._isUnboundCustomOperation(event)) {
throw new NotInModel(event, this._model.find('service')['@source'])
throw new NotInModel(event, this._model)
}

@@ -242,3 +263,3 @@

if (entity !== undefined && !this._isBoundCustomOperation(entity, event)) {
throw new NotInModel(event, this._model.find('service')['@source'])
throw new NotInModel(event, this._model)
}

@@ -245,0 +266,0 @@ }

const { NextCalledBefore } = require('../../errors')
const Base = require('./Base')
const cds = require('../../cds')
const sqliteLocales = ['de', 'fr']

@@ -41,2 +43,16 @@ /**

_checkForLocalizedEntity (event, context, handler) {
if (event === 'READ' && context.target && handler.handler.isDefault && !context.target['@odata.draft.enabled']) {
let locale = ''
if (cds.options && cds.options.kind === 'sqlite') {
const userLocale = context.user.locale
if (sqliteLocales.includes(userLocale)) {
locale = `${userLocale}.`
}
}
const localizedEntity = this._model.definitions[`localized.${locale}${context.target.name}`]
context.target = localizedEntity || context.target
}
}
_middleware (event, context, resolve, reject) {

@@ -86,5 +102,9 @@ const calledFrom = {}

if (this._match(event, context, handler)) {
const oldTarget = context.target
try {
this._measureStartTime(context, handler)
this._checkForLocalizedEntity(event, context, handler)
const result = handler.handler(context, nextOnce)

@@ -95,8 +115,11 @@

.then(() => {
context.target = oldTarget
this._measureEndTime(context, handler)
})
.catch(err => {
context.target = oldTarget
reject(err)
})
} else {
context.target = oldTarget
this._measureEndTime(context, handler)

@@ -110,2 +133,3 @@ }

// Do not measure performance in case of error
context.target = oldTarget
reject(err)

@@ -112,0 +136,0 @@ return

@@ -61,3 +61,3 @@ const { NotInModel } = require('../../errors')

if (!this._isUnboundCustomOperation(normalizedEvent)) {
throw new NotInModel(event, this._model.find('service')['@source'])
throw new NotInModel(event, this._model)
}

@@ -86,3 +86,3 @@ }

if (this._entityHasValidType(entity)) {
throw new NotInModel(entity, this._model.find('service')['@source'])
throw new NotInModel(entity, this._model)
}

@@ -92,3 +92,3 @@

if (entity.kind !== 'entity' || typeof entity.name !== 'string') {
throw new NotInModel(entity, this._model.find('service')['@source'])
throw new NotInModel(entity, this._model)
}

@@ -103,3 +103,3 @@

if (!this._isModeledEntity(serviceEntity)) {
throw new NotInModel(entityName, this._model.find('service')['@source'])
throw new NotInModel(entityName, this._model)
}

@@ -113,3 +113,3 @@

) {
throw new NotInModel(event, this._model.find('service')['@source'])
throw new NotInModel(event, this._model)
}

@@ -116,0 +116,0 @@

@@ -15,8 +15,10 @@ const cds = require('../cds')

const getDiff = require('./utils/diff')
const { checkIntegrity } = require('./utils/handlerUtils')
const { checkIntegrity, flattenDeepToOneAssociations } = require('./utils/handlerUtils')
const { isPersonalDataRelevant } = require('./utils/personalData')
const isSelectEntity = require('./utils/selectEntityUtils')
const logger = require('./utils/logger')
const compareJson = require('./utils/compareJson')
const {
events: { CUD_DRAFT }
events: { CUD_DRAFT },
DRAFT_COLUMNS
} = require('./utils/constants')

@@ -69,2 +71,3 @@

this._handlers = {
initial: new Before(this.model, this.name),
before: new Before(this.model, this.name),

@@ -104,6 +107,39 @@ on: new On(this.model, this.name),

/**
* @deprecated since version 1.11.0 - use Service.impl instead
*/
with (serviceImpl) {
return this.impl(serviceImpl)
}
/**
* Registers custom handlers.
* @param {function} impl - init function to register custom handlers.
* @param {string|object|function} serviceImpl - init function to register custom handlers.
*/
with (impl) {
impl (serviceImpl) {
if (typeof serviceImpl === 'string') {
serviceImpl = require(serviceImpl)
}
if (typeof serviceImpl === 'object') {
serviceImpl = serviceImpl[this.name]
}
if (typeof serviceImpl !== 'function') {
return this
}
if (/^class\s/.test(Function.prototype.toString.call(serviceImpl))) {
const ImplClass = serviceImpl
serviceImpl = srv => {
const inst = new ImplClass(srv)
for (let e of Reflect.ownKeys(ImplClass.prototype)) {
if (e in { constructor: 1, prototype: 1 }) {
continue
}
const handler = (...args) => inst[e](...args)
srv.on(e, handler)
}
}
}
this._handlers.before.startAddingWithHandlers()

@@ -113,5 +149,3 @@ this._handlers.on.startAddingWithHandlers()

if (typeof impl === 'function') {
impl.apply(this, [this])
}
serviceImpl.apply(this, [this])

@@ -153,3 +187,3 @@ this._handlers.before.finishAddingWithHandlers()

this._addDefaultBeforeHandler(before, entity)
this._addDefaultInitialHandler(before, entity)
this._addDefaultOnHandler(on, entity)

@@ -185,6 +219,6 @@ this._addDefaultRejectHandler(reject, entity.name)

_addDefaultBeforeHandler (before, entity) {
_addDefaultInitialHandler (before, entity) {
for (const [method, handlerName] of before) {
const handler = handlers[handlerName](this)
this.before(method, entity, this._markDefault(handler))
this._initial(method, entity, this._markDefault(handler))
}

@@ -231,3 +265,3 @@

/**
* Register default before handlers for all authorization annotated entities of the service.
* Register default before handlers for all authorization annotated entities and operations of the service.
* @private

@@ -243,20 +277,50 @@ */

}
if (entity.actions) {
// bound actions or functions
this._addBeforeOperationAuthHandler(entity.actions, entity)
}
}
this._addBeforeOperationAuthHandler(this.operations)
}
_addBeforeOperationAuthHandler (operations, entity) {
for (const key of Object.keys(operations)) {
const annotations = getAnnotations(operations[key])
if (annotations['operation']) {
const handler = handlers.beforeEntityAuth(annotations['operation'], this._auditLogger)
this.before(key, entity, this._markDefault(handler))
}
}
}
_getServiceEntities () {
const regex = new RegExp(`^${this.name.replace(/\./g, '\\.')}\\.\\w+$`)
return this.model.all(definition => {
return (
(definition.kind === 'entity' || isSelectEntity(definition, this.model)) && // OLD CSN: isSelectEntity
definition.name.match(regex)
)
})
}
return [
...this.model.each(definition => {
return (
(definition.kind === 'entity' || isSelectEntity(definition, this.model)) && // OLD CSN: isSelectEntity
definition.name.match(regex)
)
})
]
/**
* Add an initial handler for a specific event type and entity. N handlers per type and entity can be registered.
* This is reserved for generic intial handlers and not part of the registration API for custom handlers.
* @param {string} event - Name of event like 'CREATE', 'UPDATE', 'DELETE', ...
* @param {string|Object} [entity] - Name of the entity or CSN entity.
* @param {function|Object} handler - To be executed event handler. It could be function or CQN object.
* @returns {Service}
* @private
*/
_initial (event, entity, handler) {
this._handlers.initial.use(event, entity, handler)
return this
}
/**
* Add a handler for a specific event type and entity. N handlers per type and entity can be registered.
* Add a before handler for a specific event type and entity. N handlers per type and entity can be registered.
* @param {string} event - Name of event like 'CREATE', 'UPDATE', 'DELETE', ...

@@ -274,3 +338,3 @@ * @param {string|Object} [entity] - Name of the entity or CSN entity.

/**
* Replace an handler for a specific event type and entity.
* Replace an on handler for a specific event type and entity.
* @param {string} [event] - Name of event like 'CREATE', 'UPDATE', 'DELETE', ...

@@ -288,3 +352,3 @@ * @param {string|Object} [entity] - Name of the entity or CSN entity.

/**
* Add a handler for a specific event type and entity. N handlers per type and entity can be registered.
* Add an after handler for a specific event type and entity. N handlers per type and entity can be registered.
* In case an arrow function with '(each) =>' is used for the event handler an iterator for the result set will be added automatically.

@@ -329,4 +393,8 @@ * @param {string} event - Name of event like 'CREATE', 'UPDATE', 'DELETE', ...

this._addRunIfPrimarySessionExists(context)
this._addDiffIfNeeded(context)
this._addLogger(context)
// validate associations for deep insert
flattenDeepToOneAssociations(context, this.model)
let result

@@ -356,11 +424,8 @@

*/
_actionsFunctionsHandlers (event, context) {
return this._handlers.before
.executeHandlerIfListed(event, context)
.then(() => {
return 'results' in context ? context.results : this._handlers.on.executeHandlerIfListed(event, context)
})
.then(result => {
return this._handlers.after.executeHandlerIfListed(event, context, result)
})
async _actionsFunctionsHandlers (event, context) {
await this._handlers.initial.executeHandlerIfListed(event, context)
await this._handlers.before.executeHandlerIfListed(event, context)
const result =
'results' in context ? context.results : await this._handlers.on.executeHandlerIfListed(event, context)
return this._handlers.after.executeHandlerIfListed(event, context, result)
}

@@ -460,2 +525,91 @@

_createSelectColumns (entity) {
const columns = []
for (const element of Object.values(entity.elements)) {
if (element.type === 'cds.Composition') {
columns.push({
ref: [element.name],
expand: this._createSelectColumns(element._target)
})
} else if (element.type !== 'cds.Association' && !DRAFT_COLUMNS.includes(element.name)) {
columns.push({ ref: [element.name] })
}
}
return columns
}
_createWhereCondition (entity, data) {
return Object.keys(entity.keys).reduce((prev, curr) => {
if (!DRAFT_COLUMNS.includes(curr)) {
prev[curr] = data[curr]
}
return prev
}, {})
}
_calculateDiff (context) {
const {
data,
event,
run,
statements: { SELECT },
target
} = context
if (event === 'CREATE') {
return Promise.resolve(compareJson(data, [], target))
}
return run(
SELECT.from(target)
.columns(this._createSelectColumns(target))
.where(this._createWhereCondition(target, data))
).then(dbState => {
if (event === 'UPDATE') {
return compareJson(data, dbState, target)
}
// event === 'DELETE'
return compareJson([], dbState, target)
})
}
_addDiffIfNeeded (context) {
if (context.event === 'CREATE' || context.event === 'UPDATE' || context.event === 'DELETE') {
const that = this
let diff
/**
* Function to retrieve the difference from db state.
* Works for deep documents using compositions.
*
* Caches the difference after the first use.
*
* Output format is:
* {
* type: 'update',
* entity: 'entityName',
* keys: { ID: 1 },
* values: {
* old: 'A',
* new: 'B'
* }
* }
*/
context.diff = function () {
// no arrow function used on purpose
// `this` will point to the new context of local client
if (diff) {
return Promise.resolve(diff)
}
return that._calculateDiff(this).then(calculatedDiff => {
diff = calculatedDiff
return calculatedDiff
})
}
}
}
/**

@@ -462,0 +616,0 @@ * Track the general event as commit event.

@@ -24,2 +24,4 @@ const RO = ['READ']

const DRAFT_COLUMNS = ['IsActiveEntity', 'HasActiveEntity', 'HasDraftEntity', 'DraftAdministrativeData_DraftUUID']
module.exports = {

@@ -45,3 +47,4 @@ messages: {

TRANSACTION: TRANSACTION
}
},
DRAFT_COLUMNS
}
const { generateUUID } = require('@sap/cds-ql')
const { getParent } = require('./compositionTree')
const { checkNotNullAll, checkReferenceIntegrity, checkAll } = require('../../util/assert')
const { checkNotNullAll, checkReferenceIntegrity } = require('../../util/assert')
const { processDeep, processDeepAsync } = require('../../util/dataProcessUtils')

@@ -58,7 +58,7 @@ const { ensureNoDraftsSuffix, ensureDraftsSuffix } = require('./draftUtils')

const _checkAll = context => {
let hasError = false
const checkNotNull = context => {
let error = false
processDeep(
(data, entity) => {
const errors = checkAll(entity, data, context.event)
const errors = checkNotNullAll(entity, data)
if (errors.length !== 0) {

@@ -68,3 +68,3 @@ for (const error of errors) {

}
hasError = true
error = true
}

@@ -77,16 +77,88 @@ },

)
return hasError
return error
}
const checkNotNull = context => {
let error = false
const _isValidToOne = (targetEntity, data) => {
for (const property in data) {
if (!targetEntity.keys[property]) {
return false
}
}
return true
}
const _flattenToOneAssociation = (element, entity, row, property, csn) => {
if (element.is2one) {
const targetEntity = element._target
if (!element.on && !element.onCond && _isValidToOne(targetEntity, row[property])) {
for (const key in targetEntity.keys) {
row[element.name + '_' + key] = row[element.name][key]
}
delete row[element.name]
}
}
}
const _flattenDeepToOneAssociations = (entity, data, csn) => {
if (!Array.isArray(data)) {
return _flattenDeepToOneAssociations(entity, [data], csn)
}
for (const row of data) {
for (const property in row) {
const element = entity.elements[property]
if (element && element.type === 'cds.Association') {
_flattenToOneAssociation(element, entity, row, property, csn)
}
}
}
}
const flattenDeepToOneAssociations = (context, csn) => {
if (!context.target) {
return
}
if (context.event !== 'CREATE' && context.event !== 'UPDATE') {
return
}
processDeep(
(data, entity) => {
const errors = checkNotNullAll(entity, data)
if (errors.length !== 0) {
for (const error of errors) {
context.error(400, error)
}
error = true
_flattenDeepToOneAssociations(entity, data, csn)
},
context.data,
context.target,
false,
true
)
}
const _isReadOnlyFieldControl = element => {
return (
(element['@Common.FieldControl'] && element['@Common.FieldControl']['#'] === 'ReadOnly') ||
element['@Common.FieldControl.ReadOnly'] ||
element['@FieldControl.ReadOnly']
)
}
const removeReadOnlyColumns = (entity, data, event) => {
if (!Array.isArray(data)) {
return removeReadOnlyColumns(entity, [data], event)
}
for (const subData of data) {
for (const columnName in subData) {
if (subData[columnName] !== undefined && _isReadOnlyField(entity.elements[columnName], event)) {
delete subData[columnName]
}
}
}
}
const filterReadOnly = context => {
processDeep(
(data, entity) => {
removeReadOnlyColumns(entity, data, context.event)
},

@@ -98,5 +170,32 @@ context.data,

)
return error
}
const _isOnUpdateOrInsertReadOnly = (element, event) => {
return (element['@cds.on.update'] && event === 'CREATE') || (element['@cds.on.insert'] && event === 'UPDATE')
}
const _isComputedReadOnly = element => {
if (element['@Core.Computed'] && !element['@cds.on.update'] && !element['@cds.on.insert']) {
return true
}
}
const _isImmutableReadOnly = (element, event) => {
if (element['@Core.Immutable'] && !element['@cds.on.update']) {
return event === 'UPDATE'
}
}
const _isReadOnlyField = (element, event) => {
// TODO calculated fields not supported yet in cds
return (
element &&
(_isOnUpdateOrInsertReadOnly(element, event) ||
_isReadOnlyFieldControl(element) ||
_isComputedReadOnly(element) ||
_isImmutableReadOnly(element, event) ||
element['virtual'])
)
}
const checkIntegrity = async context => {

@@ -248,3 +347,4 @@ if (!context.run) {

checkIntegrity,
checkAll: _checkAll
filterReadOnly,
flattenDeepToOneAssociations
}

@@ -5,3 +5,3 @@ const { getCompositionSet, getCompositionTree } = require('./compositionTree')

const getColumns = require('../utils/columns')
const DRAFT_COLUMNS = ['IsActiveEntity', 'HasActiveEntity', 'HasDraftEntity', 'DraftAdministrativeData_DraftUUID']
const { DRAFT_COLUMNS } = require('../utils/constants')
const _getEntities = (nameSet, definitions) => {

@@ -8,0 +8,0 @@ const entities = []

@@ -19,20 +19,24 @@ const { all, resolve } = require('../util/thenable')

const ASSERT_VALID_ELEMENT = 'ASSERT_VALID_ELEMENT'
const ASSERT_VALID_VALUE = 'ASSERT_VALID_VALUE'
const ASSERT_DATA_TYPE = 'ASSERT_DATA_TYPE'
const ASSERT_ENUM = 'ASSERT_ENUM'
const ASSERT_NOT_NULL = 'ASSERT_NOT_NULL'
const ASSERT_READ_ONLY = 'ASSERT_READ_ONLY'
const ASSERT_REFERENCE_INTEGRITY = 'ASSERT_REFERENCE_INTEGRITY'
const ASSERT_DEEP_TO_ONE_ASSOCIATION = 'ASSERT_DEEP_TO_ONE_ASSOCIATION'
const ASSERT_DEEP_TO_MANY_ASSOCIATION = 'ASSERT_DEEP_TO_MANY_ASSOCIATION'
const AssertCodeText = {
ASSERT_VALID_ELEMENT: e => `Element '${e}' is not valid`,
ASSERT_VALID_VALUE: e => `Value of element '${e}' is not in specified range/format`,
ASSERT_DATA_TYPE: e => `Value of element '${e}' is invalid according to type definition`,
ASSERT_ENUM: e => `Value of element '${e}' is invalid according to enum declaration`,
ASSERT_NOT_NULL: e => `Value of element '${e}' is required`,
ASSERT_READ_ONLY: e => `Value of element '${e}' is read only`,
ASSERT_REFERENCE_INTEGRITY: e => `Reference integrity is violated for association '${e}'`
ASSERT_REFERENCE_INTEGRITY: e => `Reference integrity is violated for association '${e}'`,
ASSERT_DEEP_TO_ONE_ASSOCIATION: e => `It is not allowed to modify sub documents in to-one Association '${e}'`,
ASSERT_DEEP_TO_MANY_ASSOCIATION: e => `Deep insert is not allowed for to-many Association '${e}'`
}
const _enumValues = element => {
return Object.keys(element.enum).map(enumKey => {
const enum_ = element.enum[enumKey]
return Object.keys(element).map(enumKey => {
const enum_ = element[enumKey]
const enumValue = enum_ && (enum_.val || enum_.value) // OLD CSN

@@ -43,15 +47,6 @@ return enumValue ? enumValue['='] || enumValue : enumKey

const _getEnumElements = entity => {
return Object.keys(entity.elements)
.filter(key => entity.elements[key].enum)
.reduce((map, key) => {
map[entity.elements[key].name] = _enumValues(entity.elements[key])
return map
}, {})
}
const _assertError = (code, entity, element, value) => {
const _assertError = (code, entity, element, value, key) => {
const { name, type, precision, scale } = element
const error = new Error(AssertCodeText[code](name))
const error = new Error(AssertCodeText[code](name || key))
Object.assign(error, {

@@ -128,2 +123,12 @@ code,

const _checkInRange = (val, range) => {
return _checkISODate(val)
? (new Date(val) - new Date(range[0])) * (new Date(val) - new Date(range[1])) <= 0
: (val - range[0]) * (val - range[1]) <= 0
}
const _checkRegExpFormat = (val, format) => {
return _checkString(val) ? val.match(new RegExp(format, 'g')) : false
}
const _isAssociationOrComposition = element => element.type === 'cds.Association' || element.type === 'cds.Composition'

@@ -186,3 +191,2 @@

const element = entity.elements[key]
const enumElements = _getEnumElements(entity)

@@ -201,9 +205,21 @@ if (!element) {

// code, entity, element, value
result.push(_assertError(ASSERT_DATA_TYPE, entity, element, row[key]))
result.push(_assertError(ASSERT_DATA_TYPE, entity, element, row[key], key))
}
if (enumElements[element.name] && !enumElements[element.name].includes(row[key])) {
const enumElements = element.enum
const rangeElements = element['@assert.range']
const formatElements = element['@assert.format']
if (enumElements && !_enumValues(enumElements).includes(row[key])) {
result.push(_assertError(ASSERT_ENUM, entity, element, row[key]))
}
if (rangeElements && !_checkInRange(row[key], rangeElements)) {
result.push(_assertError(ASSERT_VALID_VALUE, entity, element, row[key]))
}
if (formatElements && !_checkRegExpFormat(row[key], formatElements)) {
result.push(_assertError(ASSERT_VALID_VALUE, entity, element, row[key]))
}
return result

@@ -237,37 +253,2 @@ }

const _isOnUpdateOrInsert = (element, event) => {
return (element['@cds.on.update'] && event === 'UPDATE') || (element['@cds.on.insert'] && event === 'CREATE')
}
const _isReadOnlyFieldControl = element => {
return (
(element['@Common.FieldControl'] && element['@Common.FieldControl']['#'] === 'ReadOnly') ||
element['@Common.FieldControl.ReadOnly'] ||
element['@FieldControl.ReadOnly']
)
}
const _isComputedReadOnly = (element, event) => {
if (element['@Core.Computed']) {
return !_isOnUpdateOrInsert(element, event)
}
}
const _isImmutableReadOnly = (element, event) => {
if (element['@Core.Immutable']) {
return !_isOnUpdateOrInsert(element, event)
}
}
const _isReadOnlyField = (element, event) => {
// TODO calculated fields not supported yet in cds
return (
(_isReadOnlyFieldControl(element) ||
_isComputedReadOnly(element, event) ||
_isImmutableReadOnly(element, event) ||
element['virtual']) &&
!_isAssociationOrComposition(element)
)
}
const checkNotNull = (entity, data) => {

@@ -314,3 +295,3 @@ if (!Array.isArray(data)) {

const _checkExistsWhere = (entity, whereList, req) => {
const _checkExistsWhere = (entity, whereList, context) => {
const checks = whereList.map(where => {

@@ -329,3 +310,3 @@ if (where.length === 0) {

return req.run(cqn).then(exists => {
return context.run(cqn).then(exists => {
return exists.length !== 0

@@ -338,5 +319,5 @@ })

const _checkExists = (entity, data, req) => {
const _checkExists = (entity, data, context) => {
if (!Array.isArray(data)) {
return _checkExists(entity, [data], req).then(result => {
return _checkExists(entity, [data], context).then(result => {
return result[0]

@@ -358,3 +339,3 @@ })

})
return _checkExistsWhere(entity, where, req)
return _checkExistsWhere(entity, where, context)
}

@@ -391,5 +372,19 @@

const checkReferenceIntegrity = (entity, data, req) => {
const _checkAssociations = (entity, element, row, context, result) => {
if ((context.event === 'CREATE' || context.event === 'UPDATE') && row[element.name] !== undefined) {
if (element.is2many) {
result.push(_assertError(ASSERT_DEEP_TO_MANY_ASSOCIATION, entity, element))
return result
}
if (element.is2one) {
result.push(_assertError(ASSERT_DEEP_TO_ONE_ASSOCIATION, entity, element))
return result
}
}
}
const checkReferenceIntegrity = (entity, data, context) => {
if (!Array.isArray(data)) {
return checkReferenceIntegrity(entity, [data], req)
return checkReferenceIntegrity(entity, [data], context)
}

@@ -399,12 +394,20 @@

const result = Object.keys(entity.elements)
.filter(
key => entity.elements[key].type === 'cds.Association' && !entity.elements[key].on && !entity.elements[key].onCond
)
.filter(key => entity.elements[key].type === 'cds.Association')
.reduce((result, key) => {
const element = entity.elements[key]
return data.reduce((result, row) => {
const assocError = _checkAssociations(entity, element, row, context, result)
if (assocError) {
return assocError
}
if (entity.elements[key].on || entity.elements[key].onCond) {
return result
}
const foreignKey = _buildForeignKey(element, row)
checks.push(
_checkExists(element._target, foreignKey, req).then(exists => {
_checkExists(element._target, foreignKey, context).then(exists => {
if (!exists) {

@@ -426,39 +429,6 @@ result.push(_assertError(ASSERT_REFERENCE_INTEGRITY, entity, element, foreignKey))

return resolve([])
return resolve(result || [])
}
const _checkValidElementsForSubData = (entity, key, subData, result) => {
for (const property in subData) {
if (!entity.elements[property]) {
result.push(_assertError(ASSERT_VALID_ELEMENT, entity, { name: key }))
}
}
}
const checkAll = (entity, data, event) => {
if (!Array.isArray(data)) {
return checkAll(entity, [data], event)
}
return Object.keys(entity.elements).reduce((result, key) => {
const element = entity.elements[key]
return data.reduce((result, subData) => {
_checkValidElementsForSubData(entity, key, subData, result)
if (subData[key] !== undefined && _isReadOnlyField(element, event)) {
result.push(_assertError(ASSERT_READ_ONLY, entity, element, subData[key]))
}
if (_isMandatoryField(element) && (subData[key] === null || subData[key] === undefined)) {
result.push(_assertError(ASSERT_NOT_NULL, entity, element, subData[key]))
}
return result
}, result)
}, [])
}
module.exports = {
checkAll,
checkStatic,

@@ -465,0 +435,0 @@ checkNotNull,

@@ -61,3 +61,3 @@ const { all } = require('../util/thenable')

if (col.default !== undefined && data[col.name] === undefined) {
data[col.name] = col.default.val || col.default // OLD CSN
data[col.name] = 'val' in col.default ? col.default.val : col.default // OLD CSN
}

@@ -64,0 +64,0 @@ }

{
"name": "@sap/cds-services",
"version": "1.11.1",
"version": "1.14.0",
"lockfileVersion": 1,

@@ -8,14 +8,14 @@ "requires": true,

"@sap/cds-hana": {
"version": "1.11.1",
"version": "1.13.0",
"requires": {
"@sap/cds-sql": "1.11.1"
"@sap/cds-sql": "1.13.0"
}
},
"@sap/cds-ql": {
"version": "1.11.1",
"version": "1.14.0",
"requires": {
"@sap/cds-hana": "1.11.1",
"@sap/cds-sql": "1.11.1",
"@sap/cds-sqlite": "1.11.1",
"generic-pool": "3.4.2",
"@sap/cds-hana": "1.13.0",
"@sap/cds-sql": "1.13.0",
"@sap/cds-sqlite": "1.13.0",
"generic-pool": "3.7.1",
"uuid": "3.3.2"

@@ -25,12 +25,12 @@ }

"@sap/cds-sql": {
"version": "1.11.1"
"version": "1.13.0"
},
"@sap/cds-sqlite": {
"version": "1.11.1",
"version": "1.13.0",
"requires": {
"@sap/cds-sql": "1.11.1"
"@sap/cds-sql": "1.13.0"
}
},
"@sap/odata-commons": {
"version": "2.1.0",
"version": "2.1.1",
"requires": {

@@ -41,5 +41,5 @@ "big.js": "=5.2.2"

"@sap/odata-server": {
"version": "1.3.3",
"version": "1.3.4",
"requires": {
"@sap/odata-commons": "^2.1.0",
"@sap/odata-commons": "^2.1.1",
"xmlbuilder": "=10.1.0"

@@ -52,3 +52,3 @@ }

"generic-pool": {
"version": "3.4.2"
"version": "3.7.1"
},

@@ -55,0 +55,0 @@ "uuid": {

@@ -1,1 +0,1 @@

{"bundleDependencies":false,"dependencies":{"@sap/cds-ql":"1.11.1","@sap/odata-server":"1.3.3"},"deprecated":false,"description":"This package handles the generation of an OData service using the provided model. It is possible to start N services per server and each service has its own endpoint. This package also offers the possibility to register custom handlers for performing create, read, update and delete operations.","engines":{"node":">= 8.9.0"},"husky":{"hooks":{"pre-commit":"lint-staged && npm run jsdoc2md"}},"lint-staged":{"{lib,test}/**/*.js":["prettier-standard","standard --fix","git add"]},"main":"lib/index.js","name":"@sap/cds-services","scripts":{"format":"prettier-standard 'lib/**/*.js' 'test/**/*.js' && standard --fix","start":"node app/app.js"},"version":"1.11.1","license":"SEE LICENSE IN developer-license-3.1.txt"}
{"bundleDependencies":false,"dependencies":{"@sap/cds-ql":"1.14.0","@sap/odata-server":"1.3.4"},"deprecated":false,"description":"This package handles the generation of an OData service using the provided model. It is possible to start N services per server and each service has its own endpoint. This package also offers the possibility to register custom handlers for performing create, read, update and delete operations.","engines":{"node":">= 8.9.0"},"husky":{"hooks":{"pre-commit":"lint-staged && npm run jsdoc2md"}},"lint-staged":{"{lib,test}/**/*.js":["prettier-standard","standard --fix","git add"]},"main":"lib/index.js","name":"@sap/cds-services","scripts":{"format":"prettier-standard 'lib/**/*.js' 'test/**/*.js' && standard --fix","start":"node app/app.js"},"version":"1.14.0","license":"SEE LICENSE IN developer-license-3.1.txt"}

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc