no-shared-handler-variable
Rule Details
Discourages sharing state between handlers using variables from parent scopes because this can lead to data leakage between tenants. This rule automatically checks handler registrations inside classes that extend cds.ApplicationService.
To enable this check for functions declared outside such classes, add a type annotation. Any function annotated with
@type {import('@sap/cds').CRUDEventHandler.Before},@type {import('@sap/cds').CRUDEventHandler.On},@type {import('@sap/cds').CRUDEventHandler.After}
will also be checked by this rule.
Examples
✅ Correct example
In the following example, only locally defined variables are used within handler implementation:
const cds = require('@sap/cds')
module.exports = class AdminService extends cds.ApplicationService { async init() {
this.after('READ', 'Books', async () => {
// local variable only, no state shared between handlers
const books = await cds.run(SELECT.from('Books'))
return books
})
this.on('CREATE', 'Books', newBookHandler)
await super.init()
}}
/** @type {import('@sap/cds').CRUDEventHandler.On} */
async function newBookHandler (req) {
const { name } = req.data
// local variable only, no state shared between handlers
const newBook = await cds.run(INSERT.into('Books').entries({ name }))
return newBook
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❌ Incorrect example
In the following example, the variables newBook and readBooks are declared in scopes surrounding the handler function, making their value available to subsequent calls of that handler. While this may seem advantageous, it can cause issues in a multitenant scenario, where the handler function can be invoked by multiple tenants.
const cds = require('@sap/cds')
let lastCreatedBook
let lastReadBooks
module.exports = class AdminService extends cds.ApplicationService { async init() {
this.after('READ', 'Books', async () => {
// variable from surrounding scope, state is shared between handler calls
lastReadBooks = await cds.run(SELECT.from('Books'))
return lastReadBooks
})
this.on('CREATE', 'Books', newBookHandler)
await super.init()
}}
/** @type {import('@sap/cds').CRUDEventHandler.On} */
async function newBookHandler (req) {
const { name } = req.data
// variable from surrounding scope, state is shared between handler calls
lastCreatedBook = await cds.run(INSERT.into('Books').entries({ name }))
return lastCreatedBook
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Caveats
The following code styles are not checked by this rule as of today:
Inline Import + Extension
The following example imports @sap/cds within the extends clause of the service implementation.
class AdminService extends require('@sap/cds').ApplicationService { /* … */ }Instead, import @sap/cds separately, as shown in the other examples.
Using Methods as Handler
Using a misbehaving class method as handler implementation will also not be detected, even if it is located in service implementation class itself:
const cds = require('@sap/cds')
class AdminService extends cds.ApplicationService {
badHandler () { /* bad things going on in here */ }
init () {
this.on('READ', 'Books', this.badHandler)
}
}Use a function instead.
Other Service-Implementation Styles Than Classes
Only classes extending cds.ApplicationService are checked as part of this rule, to avoid triggering too many false positives. So all other implementation styles will not trigger this rule:
cds.services['AdminService'].on('READ', 'Books', () => {})(or any of the old implementation patterns for services besides classes, like using cds.service.impl.)
Version
This rule was introduced in @sap/eslint-plugin-cds 4.0.2.