Socket
Socket
Sign inDemoInstall

@sap/cds-mtxs

Package Overview
Dependencies
2
Maintainers
1
Versions
53
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.4.3 to 1.4.4

srv/jobs-service.cds

54

CHANGELOG.md

@@ -9,2 +9,56 @@ # Change Log

## Version 1.4.4 - 2023-01-16
### Changed
- `cds.xt.DeploymentService` configuration has been flattened. Instead of
```js
"hdi": {
"create": {
"provisioning_parameters": {
"database_id": "<ID>"
},
"binding_parameters": {
"key": "value"
}
}
}
```
you can now also write
```js
"hdi": {
"create": {
"database_id": "<ID>"
},
"bind": {
"key": "value"
}
}
```
The old configuration is still supported, but you're advised to migrate to the new configuration for improved readability.
- `/-/cds/jobs/pollJob` now also returns a `tenants` field, so tenant-specific tasks don't have to be polled individually. An example response format looks like this:
```js
{
"status": "FAILED",
"op": "upgrade",
"tenants": {
"non-existing-tenant": {
"status": "FAILED",
"error": "Tenant 'non-existing-tenant' does not exist"
},
"existing-tenant": {
"status": "FINISHED"
}
}
}
```
### Fixed
- `cds.xt.SaasProvisioningService`: `*` is not allowed as a tenant name any more.
- Namespace check for new entities in extensions now also covers new root entities.
- Asynchronous operations now correctly send the callbacks defined via `status_callback` or `mtx_status_callback`.
## Version 1.4.3 - 2022-12-28

@@ -11,0 +65,0 @@

2

package.json
{
"name": "@sap/cds-mtxs",
"version": "1.4.3",
"version": "1.4.4",
"description": "SAP Cloud Application Programming Model - Multitenancy library",

@@ -5,0 +5,0 @@ "homepage": "https://cap.cloud.sap/",

@@ -14,15 +14,7 @@ const cds = require('@sap/cds/lib')

async init() {
// TODO: Support UPDATE event type for saas-registry
//this.on('UPDATE', 'tenant', ({data}) => data?.eventType === 'UPDATE' ? this.update : this.create)
this.on('UPDATE', 'tenant', this.create)
this.on('READ', 'tenant', this.read)
this.on('DELETE', 'tenant', this.delete)
this.on('dependencies', this.getDependencies)
this.on('getAppUrl', this.determineAppUrl)
this.on('succeeded', this.succeeded)
this.on('failed', this.failed)
this.on('upgrade', this.update)
this.on('upgradeAll', this.updateAll)
await super.init() // ensure to call super.init()
await super.init() // ensure to call super.init() // REVISIT: Why?
}

@@ -55,5 +47,5 @@

async determineAppUrl(context) {
return SaasRegistryUtil.getAppUrlFromHeaders(context.data.subscriptionHeaders)
?? SaasRegistryUtil.getAppUrlFromEnv(context.data.subscriptionPayload)
getAppUrl(subscriptionPayload, subscriptionHeaders) {
return subscriptionHeaders?.application_url
?? process.env.SUBSCRIPTION_URL?.replace(`\${tenant_subdomain}`, subscriptionPayload.subscribedSubdomain)
?? 'Tenant successfully subscribed - no application URL provided'

@@ -73,6 +65,6 @@ }

const sps = await cds.connect.to('cds.xt.SaasProvisioningService')
const appUrl = await sps.getAppUrl(context.data, context.headers)
if (isSync) {
LOG.info(`Subscribing tenant ${tenant}`)
const sps = await cds.connect.to('cds.xt.SaasProvisioningService')
const _appUrl = sps.getAppUrl(context.data, context.headers)
try {

@@ -83,13 +75,15 @@ const ds = await cds.connect.to(DeploymentService)

LOG.info(`Successfully subscribed tenant ${tenant}`)
await this.emit('succeeded', { task: 'subscribe', result: await _appUrl })
await this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)
} catch (error) {
await this.emit('failed', { task: 'subscribe', result: error })
await this._sendCallback('FAILED', 'Tenant creation failed')
throw error
}
return _appUrl
} else {
const lcs = await cds.connect.to(JobsService)
const tx = lcs.tx(context)
return tx.enqueue([new Set([tenant])], 'subscribe', [context.data, this._options(context.data)])
return appUrl
}
const js = await cds.connect.to(JobsService)
const tx = js.tx(context)
return tx.enqueue([new Set([tenant])], 'subscribe', [context.data, this._options(context.data)], error => {
if (error) this._sendCallback('FAILED', 'Tenant creation failed')
else this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)
})
}

@@ -111,9 +105,9 @@

async update(context) {
if (!context.data?.tenants?.length) return
const tenantList = context.data.tenants.includes('*') ? undefined : context.data.tenants
async upgrade(tenantsIds) {
if (!tenantsIds?.length) return
const tenantList = tenantsIds.includes('*') ? undefined : tenantsIds
const tenants = tenantList ?? (await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.from(Tenants, tenant => { tenant.ID }))
)).map(({ ID }) => ID)
const { isSync } = SaasRegistryUtil.getCallbackUrlsFromHeaders(context.http?.req)
const { isSync } = SaasRegistryUtil.getCallbackUrlsFromHeaders(cds.context.http?.req)
const {

@@ -127,15 +121,19 @@ clusterSize = 1, workerSize = 1, poolSize = 1

const ds = await cds.connect.to(DeploymentService)
const tx = await ds.tx(context)
const tx = await ds.tx(cds.context)
await this.limiter(clusterSize, dbToTenants, tenants =>
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => tx.upgrade(t))
)
await this.emit('succeeded', { task: 'upgrade' })
await this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded')
} catch (error) {
await this.emit('failed', { task: 'upgrade', result: error })
await this._sendCallback('FAILED', 'Tenant upgrade failed')
throw error
}
} else {
const lcs = await cds.connect.to(JobsService)
const tx = lcs.tx(context)
return tx.enqueue(dbToTenants, 'upgrade')
const js = await cds.connect.to(JobsService)
const tx = js.tx(cds.context)
// REVISIT: use jobs service for sync and async operations (might also be interesting for concurrency control)
return tx.enqueue(dbToTenants, 'upgrade', [], error => {
if (error) this._sendCallback('FAILED', 'Tenant upgrade failed')
else this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded')
})
}

@@ -161,3 +159,3 @@ }

await tx.unsubscribe(tenant, { metadata })
await this.emit('succeeded', { task: 'unsubscribe' })
await this._sendCallback('SUCCEEDED', 'Tenant deletion succeeded')
} catch (error) {

@@ -167,3 +165,3 @@ if (error.statusCode === 404) {

} else {
await this.emit('failed', { task: 'unsubscribe', result: error })
await this._sendCallback('FAILED', 'Tenant deletion failed')
throw error

@@ -175,13 +173,15 @@ }

const tx = lcs.tx(context)
return tx.enqueue([new Set([tenant])], 'unsubscribe', [{ metadata }])
return tx.enqueue([new Set([tenant])], 'unsubscribe', [{ metadata }], error => {
if (error) this._sendCallback('FAILED', 'Tenant deletion failed')
else this._sendCallback('SUCCEEDED', 'Tenant deletion succeeded')
})
}
}
updateAll(context) {
LOG.warn(`updateAll is deprecated. Use /-/cds/saas-provisioning/upgrade instead.`)
if (!context.data?.tenants) context.data.tenants = ['*']
return this.update(context)
upgradeAll(tenants) {
LOG.warn(`upgradeAll is deprecated. Use /-/cds/saas-provisioning/upgrade instead.`)
return this.upgrade(tenants ?? ['*'])
}
getDependencies() {
dependencies() {
return cds.env.requires['cds.xt.SaasProvisioningService']?.dependencies?.map(d => ({ xsappname: d })) ?? []

@@ -198,3 +198,3 @@ }

if (pending.length >= limit) {
await Promise.race(pending)
await Promise.race(pending) // eslint-disable-line no-await-in-loop
}

@@ -205,12 +205,4 @@ }

async succeeded(event) {
await this._sendCallback('SUCCEEDED', 'Succeeded', event.data.result)
}
async failed(event) {
await this._sendCallback('FAILED', 'Failed', event.data.result)
}
async _sendCallback(status, message, subscriptionUrl) {
const originalRequest = cds.context?._?.req
const originalRequest = cds.context?.http?.req
const {

@@ -217,0 +209,0 @@ isSync, isInternalCallback, saasCallbackUrlPath, callbackUrl, noCallback

const { URL } = require('url')
const axios = require('axios')
const cds = require('@sap/cds/lib')//, { axios } = cds.utils // REVISIT: Use axios helper in @sap/cds?
const cds = require('@sap/cds/lib')
const LOG = cds.log('mtx')

@@ -8,10 +8,2 @@

static get SUCCEEDED() {
return "SUCCEEDED"
}
static get FAILED() {
return "FAILED"
}
// TODO find out purpose of the authHeader parameter - intended to be set with internal mtx callback? Is auth header set in that case?

@@ -27,7 +19,6 @@ static async sendResult(callbackUrl, tenant, payload, authHeader) {

let headers
const headers = {}
try {
const authorization = authHeader ?? `Bearer ${await SaasRegistryUtil._getAuthToken()}`
headers = { authorization }
} catch(error) {
headers.authorization = authHeader ?? `Bearer ${await SaasRegistryUtil._getAuthToken()}`
} catch (error) {
if (!authHeader) {

@@ -60,16 +51,3 @@ LOG.warn('No authentication header for callback')

static async _getAuthToken() {
// TODO: find out why compat does not work
const credentials = cds.env.requires?.multitenancy?.credentials
// const credentials = typeof credentialsFromEnv === 'object'
// ? credentialsFromEnv
// : process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES)['saas-registry'][0].credentials : {}
// TODO: throw cds.error?
// TODO: Better error messages. 1. Diagnose -> 2. Support Proposal (i.e. tell how to fix the error)
if (!credentials) {
cds.error('No saas-registry credentials found', { status: 401 })
}
const { clientid, clientsecret, url } = credentials
const { clientid, clientsecret, url } = cds.env.requires?.multitenancy?.credentials ?? {}
if (!clientid || !clientsecret || !url) {

@@ -138,20 +116,2 @@ cds.error('No saas-registry credentials available from the application environment.', { status: 401 })

}
static getAppUrlFromHeaders(headers) {
return headers?.application_url
}
static get SUBDOMAIN_PLACEHOLDER() {
return 'tenant_subdomain'
}
static get SUBSCRIPTION_URL() {
return 'SUBSCRIPTION_URL'
}
static getAppUrlFromEnv(subscriptionPayload) {
const { subscribedSubdomain } = subscriptionPayload
const urlFromEnv = process.env[SaasRegistryUtil.SUBSCRIPTION_URL]
return urlFromEnv ? urlFromEnv.replace(`\${${SaasRegistryUtil.SUBDOMAIN_PLACEHOLDER}}`, subscribedSubdomain) : undefined
}
}

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

const Checker = require('./checker_base')
const LABELS = {

@@ -133,3 +131,3 @@ service: 'Service',

class AllowlistChecker extends Checker {
module.exports = class AllowlistChecker {
_setupPermissionList(mtxConfig, fullCsn) {

@@ -354,3 +352,1 @@ return new Allowlist(mtxConfig, fullCsn)

}
module.exports = AllowlistChecker

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

const Checker = require('./checker_base')
const AT_REQUIRES = '@requires'

@@ -38,3 +36,3 @@ const AT_RESTRICT = '@restrict'

class AnnotationsChecker extends Checker {
module.exports = class AnnotationsChecker {
check(reflectedCsn, compileDir) {

@@ -112,3 +110,1 @@ if (!reflectedCsn.extensions) {

}
module.exports = AnnotationsChecker

@@ -21,7 +21,8 @@ const cds = require('@sap/cds/lib')

const reflectedCsn = cds.reflect(extCsn)
const reflectedFullCsn = cds.reflect(fullCsn)
const compileBaseDir = cds.root
const findings = [
...new NamespaceChecker().check(reflectedCsn, fullCsn, compileBaseDir, linter_options),
...new NamespaceChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options),
...new AnnotationsChecker().check(reflectedCsn, compileBaseDir, linter_options),
...new AllowlistChecker().check(reflectedCsn, fullCsn, compileBaseDir, linter_options)
...new AllowlistChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options)
]

@@ -28,0 +29,0 @@ return findings

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

const Checker = require('./checker_base')
class NamespaceChecker extends Checker {
module.exports = class NamespaceChecker {
check(extensionCsn, fullCsn, compileDir, mtxConfig) {

@@ -82,6 +80,2 @@ let elementPrefixes = mtxConfig['element-prefix']

if (!this._hasEnclosingEntity(reflectedCsn, element)) {
return
}
const parent = this._getEnclosingEntity(reflectedCsn, element)

@@ -94,13 +88,13 @@

// check full csn for parent - if it exists, continue
// check full csn for parent
let elementName
const parentFromFullCsn = this._getEnclosingEntity(reflectedFullCsn, element)
if (!parentFromFullCsn) {
return
elementName = element.name
} else {
elementName = this._getNestedEntityName(element,parentFromFullCsn.name) || element.name
}
// checks nested element - TODO determine real parent and split off parent name
const nestedElementName = this._getNestedEntityName(element) // ,parent
for (const elementPrefix of elementPrefixes) {
if (nestedElementName.startsWith(elementPrefix)) {
if (elementName.startsWith(elementPrefix)) {
return

@@ -122,2 +116,3 @@ }

// TODO set parent entity name / check original test cases
_getEnclosingEntity(reflectedCsn, element) {

@@ -132,6 +127,4 @@ const splitEntityName = element.name.split('.')

_getNestedEntityName(element) {
const splitEntityName = element.name.split('.')
splitEntityName.shift()
return splitEntityName.join('.')
_getNestedEntityName(element, parentName) {
return element.name.replace(parentName + '.', '')
}

@@ -148,3 +141,3 @@

_createPrefixWarning(element, parent, prefixRule) {
let message = `Element '${element.name}' in '${parent.extend || parent.name}' must start with ${prefixRule}`
let message = `Element '${element.name}' ${parent ? `in '${parent.extend || parent.name}'` : ''} must start with ${prefixRule}`
return { message, element }

@@ -158,3 +151,1 @@ }

}
module.exports = NamespaceChecker

@@ -48,3 +48,12 @@ const fs = require('fs').promises

this.before(['getCsn', 'getEdmx', 'getExtCsn'], req => {
const regex = /^[A-Za-z0-9_-]*$|^\$hash$|^\*$/
const toggles = req.data?.toggles
const invalid = Array.isArray(toggles) ? toggles.find(t => !regex.test(t)) :
typeof toggles === 'string' && !regex.test(toggles) && toggles
if (invalid) return req.reject(400, `Unsupported input toggle param ${invalid}`)
})
this.on('getCsn', req => _getCsn(req))
this.on('getExtCsn', req => {

@@ -51,0 +60,0 @@ if (!main.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')

@@ -19,8 +19,8 @@ const cds = require('@sap/cds/lib')

// REVISIT: Use UPSERT instead
const { tenant:t, metadata } = req.data
if (t === t0) return await next()
const { tenant, metadata } = req.data
if (tenant === t0) return next()
await next()
try {
await cds.tx({ tenant: t0 }, tx =>
tx.run(INSERT.into(Tenants, { ID: t, metadata: JSON.stringify(metadata) }))
tx.run(INSERT.into(Tenants, { ID: tenant, metadata: JSON.stringify(metadata) }))
)

@@ -33,5 +33,4 @@ } catch (e) {

await next()
const { tenant:t } = req.data
await cds.tx({ tenant: t0 }, tx =>
tx.run(DELETE.from(Tenants).where({ID:{'=':t}}))
tx.run(DELETE.from(Tenants).where({ ID: req.data.tenant }))
)

@@ -46,5 +45,5 @@ })

//const needsT0Redeployment = ['cds_xt_tenants', 'cds_xt_jobs', 'cds_xt_tasks'].some(t => !tables.includes(t))
const columns = await ds.getColumns(t0, cds.requires.db.kind === 'hana' ? 'CDS_XT_JOBS' : 'cds_xt_Jobs')
const needsT0Redeployment = !columns.includes('error') && !columns.includes('ERROR')
await ds.tx({ tenant: t0 }, async tx => {
const columns = await tx.getColumns(t0, cds.requires.db.kind === 'hana' ? 'CDS_XT_JOBS' : 'cds_xt_Jobs')
const needsT0Redeployment = !columns.includes('error') && !columns.includes('ERROR')
if (!needsT0Redeployment) return

@@ -51,0 +50,0 @@ const csn = await cds.load(`${__dirname}/../../../db/t0.cds`)

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

const cds = require('@sap/cds/lib'), {db} = cds.requires
const cds = require('@sap/cds/lib'), {db} = cds.env.requires
const path = require('path')

@@ -62,8 +62,21 @@ module.exports = exports = { resources4, build, _imCreateParams }

function _imCreateParams(tenant, params = {}, metadata) {
const paramsFromEnv = cds.env.requires['cds.xt.DeploymentService']?.hdi?.create ?? {}
const paramsFromTenantOptions = cds.env.requires['cds.xt.DeploymentService']?.for?.[tenant]?.hdi?.create ?? {}
const createParamsFromEnv = cds.env.requires['cds.xt.DeploymentService']?.hdi?.create ?? {}
const createParamsFromTenantOptions = cds.env.requires['cds.xt.DeploymentService']?.for?.[tenant]?.hdi?.create ?? {}
const createParams = { ...createParamsFromEnv, ...createParamsFromTenantOptions, ...params?.hdi?.create }
const allParams = { ...paramsFromEnv, ...paramsFromTenantOptions, ...params?.hdi?.create }
allParams.provisioning_parameters = { ..._encryptionParams(metadata), ...allParams.provisioning_parameters }
return allParams
// @sap/instance-manager compat
const compat = 'provisioning_parameters' in createParams || 'binding_parameters' in createParams
if (compat) {
createParams.provisioning_parameters = { ..._encryptionParams(metadata), ...createParams.provisioning_parameters }
return createParams
}
// flatter @sap/cds-mtxs config
const bindParamsFromEnv = cds.env.requires['cds.xt.DeploymentService']?.hdi?.bind ?? {}
const bindParamsFromTenantOptions = cds.env.requires['cds.xt.DeploymentService']?.for?.[tenant]?.hdi?.bind ?? {}
const bindParams = { ...bindParamsFromEnv, ...bindParamsFromTenantOptions, ...params?.hdi?.bind }
return {
provisioning_parameters: { ..._encryptionParams(metadata), ...createParams },
binding_parameters: { ...bindParams }
}
}

@@ -115,3 +128,3 @@

}
cds.error(error)
throw error
}

@@ -118,0 +131,0 @@ }

@@ -120,3 +120,3 @@ const https = require('https')

const auth = certificate ? { maxRedirects: 0, httpsAgent: new https.Agent({ cert: certificate, key }) }
: { auth: { username: clientid, password: clientsecret } }
: { auth: { username: clientid, password: clientsecret } }
const authUrl = `${certurl ?? url}/oauth/token`

@@ -123,0 +123,0 @@ const data = `grant_type=client_credentials&client_id=${encodeURI(clientid)}`

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc