@sap/cds-services
Advanced tools
Comparing version 1.11.1 to 1.14.0
@@ -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 @@ |
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
487461
141
10902
8
+ Added@sap/cds-hana@1.13.0(transitive)
+ Added@sap/cds-ql@1.14.0(transitive)
+ Added@sap/cds-sql@1.13.0(transitive)
+ Added@sap/cds-sqlite@1.13.0(transitive)
+ Added@sap/odata-server@1.3.4(transitive)
+ Addedgeneric-pool@3.7.1(transitive)
- Removed@sap/cds-hana@1.11.1(transitive)
- Removed@sap/cds-ql@1.11.1(transitive)
- Removed@sap/cds-sql@1.11.1(transitive)
- Removed@sap/cds-sqlite@1.11.1(transitive)
- Removed@sap/odata-server@1.3.3(transitive)
- Removedgeneric-pool@3.4.2(transitive)
Updated@sap/cds-ql@1.14.0
Updated@sap/odata-server@1.3.4