Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions db-service/lib/SQLService.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
this.on(['DELETE'], this.onDELETE)
this.on(['CREATE ENTITY', 'DROP ENTITY'], this.onSIMPLE)
this.on(['BEGIN', 'COMMIT', 'ROLLBACK'], this.onEVENT)
this.before(['COMMIT'], this.onASSERT)
this.on(['*'], this.onPlainSQL)
return super.init()
}
Expand Down Expand Up @@ -299,6 +300,26 @@
return await this.exec(event)
}

/**
* Handler before COMMIT to validate the new database state
* @type {Handler}
*/
async onASSERT(req) {
const query = require('./assert.js')(this)
if (!query) return
const service = cds.context.tx.name
// Filter errors by transaction service as leaking error from other entities could leak information
// The database service is responsible for all entities therefor doesn't filter the targets
const errors = await this.run(query, [service === 'db' ? '' : service])
if (errors.length) for (const error of errors) {
const e = {}
try { Object.assign(e, JSON.parse(error.message)) } catch { e.message = e.message }

Check failure on line 316 in db-service/lib/SQLService.js

View workflow job for this annotation

GitHub Actions / Tests (22)

'e.message' is assigned to itself
// REVISIT: UI5 doesn't respect the full path target
e.target = 'in/' + error.target.slice(error.target.lastIndexOf('/') + 1)
req.error(e)
}
}

/**
* Handler for SQL statements which don't have any CQN
* @type {Handler}
Expand Down
39 changes: 39 additions & 0 deletions db-service/lib/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const cds = require('@sap/cds')

module.exports = function (db) {
const { model } = db
const asserts = []
for (const entity of model.each(cds.builtin.classes.entity)) {
const target = !entity.keys
? { val: null, param: false, as: 'target' }
: {
func: 'concat',
as: 'target',
args: [
{ val: entity.name, param: false },
{ val: '(' }, ...Object.keys(entity.keys).filter(k => !entity.keys[k].virtual && !entity.keys[k].isAssociation).map((k, i) => [{ val: (i ? ',' : '') + k + '=', param: false }, { ref: [k] }]).flat(), { val: ')' }
]
}
for (const element of entity.elements) {
if (!element['@assert']) continue
asserts.push(
cds.ql.SELECT([
{ __proto__: element['@assert'], as: 'message' },
{ ...target, args: [...target.args, { val: '/' + element.name }] },
]).from(entity)
)
}
}
if (asserts.length === 0) return

const sqls = []
const CQN2SQL = db.class.CQN2SQL
for (const query of asserts) {
const q = db.cqn4sql(query)
const renderer = new CQN2SQL(q)
renderer.SELECT(q)
sqls.push(renderer.sql)
}

return `SELECT * FROM (${sqls.join(' UNION ALL ')}) WHERE message IS NOT NULL AND target like (? || '%')`
}
51 changes: 51 additions & 0 deletions db-service/lib/cql-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ const cds = require('@sap/cds')

// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
const StandardFunctions = {

/**
* Generates SQL statement that produces an runtime compatible error object
* @param {string|object} code - The i18n code or message of the error object
* @param {Array<xpr>} args - The arguments to apply to the i18n string
* @return {string} - SQL statement
*/
error: function (code, ...args) {
let targets, message
if (typeof code === 'object') {
args = code.args?.list
targets = code.targets?.list
message = code.message
code = code.code
}

return `(${this.SELECT({
SELECT: {
expand: 'root',
columns: [
args ? {
func: 'json_array',
args: args,
as: 'args',
element: cds.builtin.types.Map,
} : { val: null, as: 'args' },
targets ? {
func: 'json_array',
args: targets,
as: 'targets',
element: cds.builtin.types.Map,
} : { val: null, as: 'targets' },
{
__proto__: (message || { val: null }),
as: 'message',
},
{
__proto__: (code || { val: null }),
as: 'code',
},
]
},
elements: {
args: cds.builtin.types.Map,
targets: cds.builtin.types.Map,
message: cds.builtin.types.String,
code: cds.builtin.types.String,
}
})})`
},

/**
* Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns
* @param {string} ref - The reference object containing column information
Expand Down
2 changes: 1 addition & 1 deletion db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@ class CQN2SQLRenderer {
} else {
cds.error`Invalid arguments provided for function '${func}' (${args})`
}
const fn = this.class.Functions[func]?.apply(this, args) || `${func}(${args})`
const fn = this.class.Functions[func]?.apply(this, Array.isArray(args) ? args: [args]) || `${func}(${args})`
if (xpr) return `${fn} ${this.xpr({ xpr })}`
return fn
}
Expand Down
Loading