Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@sap/cds-mtxs

Package Overview
Dependencies
Maintainers
0
Versions
62
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-mtxs - npm Package Compare versions

Comparing version 2.0.6 to 2.1.0

2

bin/cds-mtx-migrate.js
#!/usr/bin/env node
/* eslint-disable no-console */
async function run(param, options) {

@@ -6,0 +4,0 @@ const cds = require('@sap/cds')

#!/usr/bin/env node
/* eslint-disable no-console */

@@ -4,0 +3,0 @@ const cds = require('@sap/cds')

11

CHANGELOG.md

@@ -9,6 +9,15 @@ # Change Log

## Version 2.0.6 - 2024-08-15
## Version 2.1.0 - 2024-08-29
### Fixed
- On-the-fly CSN calculations are only done with `extensibility: true` in the main app.
- The request correlation ID is appended to generic HDI deployment error messages.
- Asynchronous extension activation now reverts faulty extensions correctly.
- Parallel extension activation calls do no longer create inconsistent extension states.
## Version 2.0.6 - 2024-08-16
### Fixed
- Subscription Manager service subscriptions are now working again.

@@ -15,0 +24,0 @@ - The `cds.features.assertIntegrity` is correctly added to the compiler options for HANA builds.

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

try {
return await fn() // eslint-disable-line no-await-in-loop
return await fn()
} catch (error) {

@@ -42,3 +42,3 @@ if (error.code === 400) throw error

LOG.error('attempt', errorCount, 'errored with', error, '- retrying attempt', errorCount + 1, 'of', retryCount)
await promisify(setTimeout)(retryGap) // eslint-disable-line no-await-in-loop
await promisify(setTimeout)(retryGap)
retryGap *= 1.5

@@ -45,0 +45,0 @@ }

{
"name": "@sap/cds-mtxs",
"version": "2.0.6",
"version": "2.1.0",
"description": "SAP Cloud Application Programming Model - Multitenancy library",

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

const cds = require('@sap/cds/lib')
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx')
const main = require('../config')
const migration = require('../../lib/migration/migration')

@@ -13,3 +14,2 @@ const DeploymentService = 'cds.xt.DeploymentService'

const { t0 = 't0' } = cds.env.requires.multitenancy
const isExtensible = !!cds.requires.extensibility || !!cds.requires['cds.xt.ExtensibilityService']

@@ -97,3 +97,3 @@ module.exports = class ProvisioningService extends cds.ApplicationService {

const tx = js.tx({ tenant: context.tenant, user: new cds.User.Privileged() })
return tx.enqueue('subscribe', [new Set([tenant])], { data, options }, error => {
return tx.enqueue('cds.xt.DeploymentService', 'subscribe', [new Set([tenant])], { data, options }, error => {
if (error) this._sendCallback('FAILED', 'Tenant creation failed')

@@ -140,3 +140,3 @@ else this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)

const all = tenantIds.includes('*')
const sharedGenDir = !isExtensible
const sharedGenDir = !main.requires.extensibility
if (sharedGenDir) (options ??= {}).skipResources ??= sharedGenDir

@@ -175,3 +175,3 @@ const tenants = all ? await this._getTenants() : tenantIds

// REVISIT: use jobs service for sync and async operations (might also be interesting for concurrency control)
return tx.enqueue('upgrade', dbToTenants, { options }, error => {
return tx.enqueue('cds.xt.DeploymentService', 'upgrade', dbToTenants, { options }, error => {
if (error) this._sendCallback('FAILED', 'Tenant upgrade failed')

@@ -218,3 +218,3 @@ else this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded')

const tx = lcs.tx({ tenant: context.tenant, user: new cds.User.Privileged() })
return tx.enqueue('unsubscribe', [new Set([tenant])], { metadata }, error => {
return tx.enqueue('cds.xt.DeploymentService', 'unsubscribe', [new Set([tenant])], { metadata }, error => {
if (error) this._sendCallback('FAILED', 'Tenant deletion failed')

@@ -264,3 +264,3 @@ else this._sendCallback('SUCCEEDED', 'Tenant deletion succeeded')

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

@@ -267,0 +267,0 @@ }

@@ -20,3 +20,3 @@ const cds = require ('@sap/cds/lib')

plugins.forEach (p => DEBUG ('\x1b[2m ', local(p), '\x1b[0m'))
console.debug() // eslint-disable-line no-console
console.debug()
loaded.forEach (({ file, module:m }) => { if (m.activated) {

@@ -23,0 +23,0 @@ DEBUG ('activated deployer plugin:', { for: m.activated, impl: local(file) })

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

const { getMigratedProjects } = require('../lib/migration/migration')
const { runLinter } = require('./extensibility/utils')
const LOG = cds.log('mtx')
module.exports = class ExtensibilityService extends cds.ApplicationService {
async init() {
async init() {
this.on('push', push)

@@ -49,6 +51,40 @@ this.on('pull', pull)

}
})
})
return super.init()
}
}
// from push.js
async activateExtension(tenant, tag, extCsn, bundles, csvs, sources, activate) {
try {
// remove current extension with tag
if (tag) await DELETE.from('cds.xt.Extensions').where({ tag })
// insert and activate extension
const ID = cds.utils.uuid()
await INSERT.into('cds.xt.Extensions').entries({
ID,
csn: JSON.stringify(extCsn),
i18n: bundles ? JSON.stringify(bundles) : null,
sources,
activated: activate,
tag
})
// do validation after extension table update - trust transaction handling for rollback
// extension linters
await runLinter(tenant, extCsn, tag)
if (activate === 'database') {
LOG.info(`activating extension '${tag}' ...`)
const { 'cds.xt.DeploymentService': ds } = cds.services
await ds.extend(tenant, csvs)
}
} catch (error) {
// needs to be serialized because it is stored in the db by the job service - TODO check for HDI error somehow?
if (error.code || error.status) throw new Error(JSON.stringify(error))
throw new Error(JSON.stringify({ status: 422, message: error.message })) // for HDI errors
}
}
}
const cds = require('@sap/cds/lib')
const { getCompilerError } = require('../../lib/utils')
const activate = async function (tenant, csvs, async) {
if (tenant) cds.context = { tenant }
const activate = async function (tenant, tag, extCsn, bundles, csvs, sources, activate, req) {
const async = cds.context.http?.req?.headers?.prefer === 'respond-async'
try {
const js = await cds.connect.to('cds.xt.JobsService')
return await new Promise((resolve, reject) => {
if (async) {
const { 'cds.xt.JobsService': js } = cds.services
const tx = js.tx({ tenant: tenant ?? cds.context.tenant, user: new cds.User.Privileged() })
return tx.enqueue('extend', [new Set([tenant])], [csvs])
} else {
const { 'cds.xt.DeploymentService': ds } = cds.services
await ds.extend(tenant, csvs)
const cb = !async ? error => {
if (error) {
try {
const errorObject = JSON.parse(error)
return reject(errorObject)
} catch {
return reject(error)
}
}
cds.context.http?.res.status(204)
return resolve()
} : () => {}
const tx = js.tx({ tenant: req.tenant, user: new cds.User.Privileged() })
const jobs = tx.enqueue('cds.xt.ExtensibilityService', 'activateExtension', [new Set([tenant])], { tag, extCsn, bundles, csvs, sources, activate }, cb)
if (async) {
cds.context.http?.res.status(202)
resolve(jobs)
}
})
} catch (err) {
if (err.code === 'ERR_CDS_COMPILATION_FAILURE') req.reject(422, getCompilerError(err.messages))
else req.reject(err)
}
}
module.exports = activate
module.exports = { activate }

@@ -6,2 +6,4 @@ const cds = require('@sap/cds/lib')

const isAsync = function () { return cds.context.http?.req?.headers?.prefer === 'respond-async' }
const readExtension = async function (req) {

@@ -20,4 +22,9 @@ const tenant = _tenant(req)

// set handles the tenant switch in that case
await set_(req, { extension: [...req.data.csn], resources: req.data.i18n, tag: req.data.ID, tenant: tenant ?? req.tenant })
const job = await set_(req, { extension: [...req.data.csn], resources: req.data.i18n, tag: req.data.ID, tenant: tenant ?? req.tenant })
if (isAsync()) {
cds.context.http?.res.status(202)
return job
}
const res = await SELECT.one.from('cds.xt.Extensions').where({ tag: req.data.ID })
cds.context.http?.res.status(200)
return { ID: res.tag, csn: res.csn, i18n: res.i18n !== '{}' ? res.i18n : undefined, timestamp: res.timestamp }

@@ -33,3 +40,7 @@ }

// leave tombstone for deployment - ID cannot be used in case of all extensions (no ID passed)
await set_(req, { extension: ['{}'], tag: TOMBSTONE_ID, tenant: tenant ?? req.tenant })
const job = await set_(req, { extension: ['{}'], tag: TOMBSTONE_ID, tenant: tenant ?? req.tenant })
if (isAsync()) {
cds.context.http?.res.status(202)
return job
}

@@ -36,0 +47,0 @@ return result

@@ -18,4 +18,4 @@ const cds = require('@sap/cds/lib')

let x
for (let p of LINTER_OPTIONS) if ((x = conf[p] || compat[p])) linter_options[p] = x // eslint-disable-line no-cond-assign
for (let p of LEGACY_OPTIONS) if ((x = compat[p])) linter_options[p] = x // eslint-disable-line no-cond-assign
for (let p of LINTER_OPTIONS) if ((x = conf[p] || compat[p])) linter_options[p] = x
for (let p of LEGACY_OPTIONS) if ((x = compat[p])) linter_options[p] = x
const hasConfig = Object.keys(linter_options).length

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

@@ -6,5 +6,5 @@ const cds = require('@sap/cds/lib'), { fs, path, tar, rimraf } = cds.utils

const activate = require('./activate')
const { activate } = require('./activate')
const { addCodeAnnotations } = require('./code-extensibility/addCodeAnnotProd')
const { readData, runLinter } = require('./utils')
const { readData } = require('./utils')

@@ -63,25 +63,5 @@ const TEMP_DIR = fs.realpathSync(require('os').tmpdir())

// remove current extension with tag
if (tag) await DELETE.from('cds.xt.Extensions').where({ tag })
// insert and activate extension
const ID = cds.utils.uuid()
await INSERT.into('cds.xt.Extensions').entries({
ID,
csn: JSON.stringify(extCsn),
i18n: bundles ? JSON.stringify(bundles) : null,
sources,
activated: 'database',
tag
})
// do validation after extension table update - trust transaction handling for rollback
// extension linters
await runLinter(tenant, extCsn, tag, req)
LOG.info(`activating extension '${tag}' ...`)
const async = cds.context.http?.req?.headers?.prefer === 'respond-async'
await activate(tenant, csvs, async)
await activate(tenant, tag, extCsn, bundles, csvs, sources, 'database', req)
}
module.exports = { push, pull }
const cds = require('@sap/cds/lib')
const LOG = cds.log('mtx')
const { activate: activateExt} = require('./activate')
const { getCompilerError } = require('../../lib/utils')
const activateExt = require('./activate')
const { runLinter } = require('./utils')
const _isCSN = str => str.substring(0, 1) === '{'
const _validateInput = function (req, extension) {
if (!req.user.is('internal-user') && req.data.tenant && req.data.tenant !== req.tenant)
req.reject(403, `No permission to add extensions to tenants other than ${req.tenant}`)
// if (!req.user.is('internal-user') && req.data.tenant && req.data.tenant !== req.tenant)
// req.reject(403, `No permission to add extensions to tenants other than ${req.tenant}`)
if (!extension) req.reject(400, 'Property "extension" is missing')
if (!extension) throw new cds.error ({ message: 'Property "extension" is missing', code: 400 })
if (Array.isArray(extension)) {
if (!extension.length) req.reject(400, 'Property "extension" is empty')
if (!extension.length) throw new cds.error ({ message: 'Property "extension" is empty', code: 400 })
} else {
const length = typeof extension === 'string' ? extension.length : Object.keys(extension).length
if (!length) req.reject(400, 'Property "extension" is malformed')
if (!length) throw new cds.error ({ message: 'Property "extension" is malformed', code: 400 })
}

@@ -45,3 +41,3 @@ }

const _getFiles = function (resources, req) {
const _getFiles = function (resources) {
let bundles = {}

@@ -54,3 +50,3 @@ let fromJson = false

if (key) {
if (fromJson) req.reject(422, `Mixed i18n file types not supported: i18n.json and ${file.name}`)
if (fromJson) throw new cds.error ({ message: `Mixed i18n file types not supported: i18n.json and ${file.name}`, code: 422 })
bundles[key[1]] = _toJson(file.content)

@@ -61,3 +57,3 @@ return

if (key) {
if (Object.entries(bundles).length) req.reject(422, `Mixed i18n file types not supported: i18n.json and .properties`)
if (Object.entries(bundles).length) throw new cds.error ({ message:`Mixed i18n file types not supported: i18n.json and .properties`, code: 422 })
try {

@@ -67,3 +63,3 @@ bundles = JSON.parse(file.content)

} catch (e) {
req.reject(422, `Invalid json content in i18n.json: ${e.message}`)
throw new cds.error ({ message: `Invalid json content in i18n.json: ${e.message}`, code: 422 })
}

@@ -82,27 +78,3 @@ return

const _addExtension = async function (extCsn, tag, bundles, csvs, tenant, activate, req) {
if (tenant) cds.context = { tenant }
const ID = cds.utils.uuid()
await cds.db.run(
INSERT.into('cds.xt.Extensions').entries([{
ID,
tag,
i18n: bundles ? JSON.stringify(bundles) : null,
csn: JSON.stringify(extCsn),
activated: activate }])
)
LOG.info(`validating extension with tag '${tag}' ...`)
try {
if (activate === 'database') {
LOG.info(`activating extension to '${activate}' ...`)
await activateExt(tenant, csvs)
}
} catch (err) {
if (err.code === 'ERR_CDS_COMPILATION_FAILURE') req.reject(422, getCompilerError(err.messages))
else req.reject(400, err.message)
}
}
const setExtension = async function (req) {
const setExtension = async function(req) {
let { extension, tag, resources, activate } = req.data

@@ -114,3 +86,3 @@ const tenant = (req.user.is('internal-user') && req.data.tenant) || req.tenant || '' // revisit magic

const set_ = async function (req, { extension, tag, resources, activate, tenant }) {
_validateInput(req, extension)
_validateInput(undefined, extension)

@@ -125,10 +97,10 @@ if (tenant) cds.context = { tenant }

if (typeof ext === 'string') {
if (!ext.length) req.reject(400, 'Missing extension')
if (!ext.length) throw new cds.error ({ message: 'Missing extension', code: 400 })
if (_isCSN(ext)) extCsn = _mergeCSN(JSON.parse(ext), extCsn)
else try { extCsn = _mergeCSN(cds.parse.cdl(ext), extCsn) } catch (e) {
if (e.code === 'ERR_CDS_COMPILATION_FAILURE') req.reject(422, e.message)
if (e.code === 'ERR_CDS_COMPILATION_FAILURE') throw new cds.error ({ message: e.message, code: 422 })
else throw e
}
} else {
if (!Object.keys(ext).length) req.reject(400, 'Missing extension')
if (!Object.keys(ext).length) throw new cds.error ({ message: 'Missing extension', code: 400 })
extCsn = _mergeCSN(ext, extCsn)

@@ -139,10 +111,7 @@ }

// call linter
await runLinter(tenant, extCsn, tag, req)
const { bundles, csvs } = _getFiles(resources)
const { bundles, csvs } = _getFiles(resources, req)
if (tag) await DELETE.from('cds.xt.Extensions').where({ tag })
await _addExtension(extCsn, tag, bundles, csvs, tenant, activate, req)
await activateExt(tenant, tag, extCsn, bundles, csvs, null, activate, req)
}
module.exports = { set_, setExtension }
const cds = require('@sap/cds/lib');
const { getAuthProvider } = require('./authProvider/AuthProviderFactory');
const AuthProvider = require('./authProvider/AuthProvider');

@@ -5,0 +4,0 @@ const LOG = cds.log('mtx');

@@ -30,3 +30,3 @@ const { path, tar, read, readdir } = cds.utils

const runLinter = async function (tenant, extCsn, tag, req) {
const runLinter = async function (tenant, extCsn, tag) {

@@ -41,3 +41,3 @@ LOG.info(`validating extension '${tag}' ...`)

} catch (err) {
return req.reject(400, getCompilerError(err.messages))
throw new cds.error ({ message: getCompilerError(err.messages), code: 400 })
}

@@ -49,3 +49,3 @@

message += findings.map(f => ' - ' + f.message).join('\n') + '\n'
return req.reject(422, message)
throw new cds.error ({ message, status: 422 })
}

@@ -52,0 +52,0 @@

@@ -11,3 +11,3 @@ const { inspect } = require('util')

queueSize = 100, clusterSize = 1, workerSize = 1, poolSize = 1
} = cds.env.requires.multitenancy.jobs
} = cds.env.requires.multitenancy?.jobs
?? cds.env.requires['cds.xt.SaasProvisioningService']?.jobs

@@ -55,6 +55,6 @@ ?? cds.env.requires['cds.xt.SmsProvisioningService']?.jobs

})
super.init()
return super.init()
}
async enqueue(op, clusters, args, onJobDone) {
async enqueue(service, op, clusters, args, onJobDone) {
const _inspect = obj => obj && Object.values(obj).filter(Boolean).length > 0 ? inspect(obj, { depth: 5, colors: true }) : []

@@ -72,3 +72,3 @@ const inspected = Object.entries(args).reduce((acc, [k, v]) => {

LOG.info(`enqueuing`, { op }, 'for', _format(clusters), ..._args)
LOG.info(`enqueuing`, { service, op }, 'for', _format(clusters), ..._args)

@@ -84,4 +84,4 @@ const job_ID = uuid()

jobQueue.enqueue({ job_ID, clusters: jobs, fn: task => {
const { 'cds.xt.DeploymentService': ds } = cds.services
return ds.tx({ tenant: cds.context.tenant }, tx => tx[op](task.tenant, ...Object.values(args)))
const serviceInstance = cds.services[service]
return serviceInstance.tx({ tenant: cds.context.tenant }, tx => tx[op](task.tenant, ...Object.values(args)))
}, onJobDone })

@@ -142,3 +142,3 @@ pickJob()

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

@@ -217,3 +217,3 @@ }

)
}, cds.env.requires.multitenancy.jobCleanupInterval ?? 24*hours)
}, cds.env.requires.multitenancy?.jobCleanupInterval ?? 24*hours)
jobCleanup.unref()

@@ -225,3 +225,3 @@

await t0_(DELETE.from(Jobs, { createdAt: { '<': cutoff.toISOString() }}))
}, cds.env.requires.multitenancy.jobCleanupIntervalStale ?? 48*hours)
}, cds.env.requires.multitenancy?.jobCleanupIntervalStale ?? 48*hours)
jobCleanupStale.unref()

@@ -228,0 +228,0 @@

@@ -16,5 +16,2 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires

// FIXME: Do that check in a better way, as ExtensibilityService is always on with mtx-sidecar preset
const isExtensible = cds.requires.extensibility || cds.requires['cds.xt.ExtensibilityService']
if (db?.kind === 'hana') {

@@ -137,6 +134,5 @@ if (!db.credentials?.sm_url) cds.error('No Service Manager credentials found. Make sure the application is bound to a BTP Service Manager instance.')

async function resources4 (out) {
const { 'cds.xt.ModelProviderService': mp } = cds.services
try {
const rscs = await mp.getResources(true)
const rscs = await mp.getResources()
await tar.xz(rscs).to(out)

@@ -155,3 +151,3 @@ return out

}
module.exports.resources4 = resources4 // required in SPS to prepare shared deployment directory
module.exports.resources4 = resources4 // required in abstract provisioning service to prepare shared deployment directory

@@ -169,6 +165,6 @@ async function csvs4(tenant, outRoot) {

}
module.exports.csvs4 = csvs4 // required in SPS to prepare shared deployment directory
module.exports.csvs4 = csvs4 // required in abstract provisioning service to prepare shared deployment directory
async function _readExtCsvs(tenant) {
if (!isExtensible) return
if (!main.requires.extensibility) return
const { 'cds.xt.ModelProviderService': mp } = cds.services

@@ -254,3 +250,4 @@ const extensions = await mp.getExtResources(tenant)

// Can already start getting the csn if later required
const _csn = isExtensible && !csnFromParameter && !skipExt ? csn4(tenant) : csnFromParameter
const requiresCsn = main.requires.extensibility && !csnFromParameter && !skipExt
const _csn = requiresCsn ? csn4(tenant) : csnFromParameter

@@ -257,0 +254,0 @@ // 1. Unpack what comes from getResources()

@@ -35,3 +35,3 @@ const { join } = require('path')

if (response?.exitCode) {
let message = `HDI deployment failed with exit code ${response.exitCode}`
let message = `HDI deployment failed with exit code ${response.exitCode}. Correlation ID: ${cds.context.id}`
if (response.signal) message += `. ${response.signal}`

@@ -38,0 +38,0 @@ return reject(new Error(message))

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

do {
// eslint-disable-next-line no-await-in-loop
const { data } = await fetchApi('service_bindings', {

@@ -169,0 +168,0 @@ params: { token, labelQuery, fieldQuery }

@@ -34,3 +34,3 @@ const cds = require('@sap/cds/lib'), {db} = cds.requires, {fs, rimraf, path} = cds.utils

LOG.info (`(re-)deploying SQLite database for tenant: ${t}`)
const deployOptions = cds.requires.extensibility ? { schema_evolution: 'auto' } : {}
const deployOptions = main.requires.extensibility ? { schema_evolution: 'auto' } : {}
const t0 = cds.requires.multitenancy?.t0 ?? 't0'

@@ -37,0 +37,0 @@ if (t !== t0) Object.assign(deployOptions, main.env.cdsc)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc