Socket
Socket
Sign inDemoInstall

@sap/cds-mtxs

Package Overview
Dependencies
Maintainers
1
Versions
61
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 1.11.0 to 1.12.0

22

CHANGELOG.md

@@ -9,3 +9,25 @@ # Change Log

## Version 1.12.0 - 2023-10-05
### Added
- Beta: Setting `cds.requires.multitenancy.humanReadableInstanceName = true` will create human-readable tenant IDs for instances created via Service Manager, instead of hashed strings. This is incompatible with existing tenants using hashed instance names.
- Extensions of internal entities / services starting with `cds.xt.` are no longer allowed.
- If no extensions exist, `ModelProviderService.getEdmx()` now uses edmx files prebuilt by `cds build`.
- `ModelProviderService.getEdmx()` with empty `locale` parameter returns non-translated edmx.
- `ModelProviderService.getI18n()` returns the translated texts for a given `locale` and `tenant`.
- Migrated extension projects can now be downloaded using `cds extend <url> --download-migrated-projects`.
### Changed
- The allowlist for HDI deployment parameters (`HDI_DEPLOY_OPTIONS`) is removed. Any option can now be used.
- `PUT /-/cds/saas-provisioning/tenant` now also allows non-UUID strings for `subscribedSubaccountId`.
### Fixed
- Synchronous call of `PUT /-/cds/saas-provisioning/tenant` now returns correct `content-type`: `text/plain`.
- Update of extensions with different tags that depend on each other does no longer result in a compilation error.
- Extensions containing new entities with associations to base entities do no longer cause a compilation error.
- For HANA deployment, no recompilation is done any more for applications not using extensibility.
## Version 1.11.0 - 2023-09-01

@@ -12,0 +34,0 @@

77

lib/migration/migration.js

@@ -6,5 +6,7 @@ const path = require('path')

const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx')
const { mkdirp, fs: fsUtils } = require('@sap/cds').utils
const fs = fsUtils.promises
const { mkdirp, rimraf, tar, exists, readdir, read, write, isdir, isfile } = cds.utils
const fs = cds.utils.fs.promises
const linter = require('../../srv/extensibility/linter')
// REVISIT: Exported private functions

@@ -357,2 +359,73 @@ module.exports._hasMtEnv = () => cds.requires.multitenancy || (cds.env.requires['cds.xt.DeploymentService'] && cds.env.requires['cds.xt.ModelProviderService'])

if (diffMessages.length) throw new Error(`Verification error for tenant ${tenant}:\n${diffMessages.join('\n')}`)
}
module.exports.getMigratedProjects = async (req, tagRule, tenant) => {
const temp = await mtxAdapter.mkdirTemp()
// run dry migration with force + own temp directory
// { directory, dry, force, tagRule: tagRegex, tag: defaultTag, "skip-verification": skipVerification, "ignore-migrations": ignoreMigrations }
await module.exports.migrate([tenant], { directory: temp, dry: true, skipVerification: true, tagRule, force: true})
const projectsLocation = path.join(temp, tenant)
if (!exists(projectsLocation)) req.reject(404, `No migrated projects found for tenant ${tenant}`)
// postprocessing: remove cds-based base model, adjust references
await module.exports.fixBaseModelReferences(projectsLocation)
try {
return await tar.cz(projectsLocation)
} finally {
rimraf(temp)
}
}
module.exports.fixBaseModelReferences = async (directory) => {
await traverse(directory, async (file) => {
// remove base cds files
if (/.*\/node_modules\/_base\//.test(file)) {
await rimraf(file)
}
// remove gen folder
if (new RegExp(`${directory}\/.*\/gen$`).test(file)) {
await rimraf(file)
}
// fix using statements
if (/.*\.cds$/.test(file) && isfile(file)) {
const content = await read(file, 'utf-8')
const fixedContent = content.replace(/\'_base\/.*\'/, '\'_base\'/g')
await write(file, fixedContent, 'utf-8')
}
})
// add index.csn and .cdsrc.json
await traverse(directory, async (file) => {
if (/.*\/node_modules\/_base$/.test(file)) {
await addBaseAndConfig(file)
}
})
async function traverse(dir, visitor) {
const list = await readdir(dir)
for (const entry of list) {
const fullPath = path.join(dir, entry)
await visitor(fullPath)
if (await isdir(fullPath)) {
await traverse(fullPath, visitor)
}
}
}
}
const addBaseAndConfig = async function (directory) {
const { 'cds.xt.ModelProviderService': mps } = cds.services
const csn = await mps.getCsn({
base: true, // without any custom extensions
flavor: 'xtended'
})
await write(path.join(directory, 'index.csn'), cds.compile.to.json(csn))
const config = linter.configCopyFrom(cds.env)
await write(path.join(directory, '.cdsrc.json'), JSON.stringify(config, null, 2))
}

11

lib/utils.js

@@ -105,12 +105,2 @@ const { readdir, isdir } = require('@sap/cds').utils

const collectFiles = async (dir, extensions) => {
const files = []
await Promise.all((await readdir(dir)).map(async dirent => {
const abs = path.join(dir, dirent)
if (isdir(abs) && !abs.includes('node_modules')) files.push(...await collectFiles(abs, extensions))
else if (!extensions || extensions.includes(path.extname(abs))) files.push(abs)
}))
return files
}
// REVISIT: opt-in retry mechanism on runtime layer?

@@ -144,4 +134,3 @@ // Sketch: cds.tx({ tenant: 't1', retries: 2 }, ...)

getCompilerError,
collectFiles,
retry
}
{
"name": "@sap/cds-mtxs",
"version": "1.11.0",
"version": "1.12.0",
"description": "SAP Cloud Application Programming Model - Multitenancy library",

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

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

await this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)
cds.context.http.res.set('content-type', 'text/plain')
} catch (error) {

@@ -76,0 +77,0 @@ await this._sendCallback('FAILED', 'Tenant creation failed')

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

const { getMigratedProjects } = require('../lib/migration/migration')
module.exports = class ExtensibilityService extends cds.ApplicationService {

@@ -29,2 +31,12 @@

this.on('getMigratedProjects', (req) => {
const { tagRule } = req.data
// REVIEW check if access for arbitrary tenants needed
const tenant = req.tenant
if (!tenant) req.reject(401, 'User not assigned to any tenant')
return getMigratedProjects(req, tagRule, tenant)
})
const _in_prod = process.env.NODE_ENV === 'production'

@@ -31,0 +43,0 @@ if (main.requires.extensibility?.code && !_in_prod && !main.requires.multitenancy) {

const cds = require('@sap/cds/lib')
const LOG = cds.log('mtx')
const { validateExtension } = require('./validation')
const { getCompilerError } = require('../../lib/utils')
const handleDefaults = require('./defaults')

@@ -66,6 +66,2 @@ const activateExt = require('./activate')

const _addExtension = async function (extCsn, tag, bundles, csvs, tenant, activate, req) {
LOG.info(`validating extension '${tag}' ...`)
const { 'cds.xt.ModelProviderService': mps } = cds.services
const csn = await mps.getCsn(tenant, ['*'])
validateExtension(extCsn, csn, req)

@@ -82,7 +78,16 @@ if (tenant) cds.context = { tenant }

)
const njCsn = cds.compile.for.nodejs(csn)
LOG.info(`activating extension to '${activate}' ...`)
if (activate === 'propertyBag' && extCsn.extensions)
extCsn.extensions.forEach(async ext => await handleDefaults(ext, njCsn))
if (activate === 'database') await activateExt(ID, tag, tenant, csvs)
LOG.info(`validating extension '${tag}' ...`)
const { 'cds.xt.ModelProviderService': mps } = cds.services
try {
const csn = await mps.getCsn(tenant, ['*'])
const njCsn = cds.compile.for.nodejs(csn)
LOG.info(`activating extension to '${activate}' ...`)
if (activate === 'propertyBag' && extCsn.extensions)
extCsn.extensions.forEach(async ext => await handleDefaults(ext, njCsn))
if (activate === 'database') await activateExt(ID, tag, tenant, csvs)
} catch (err) {
req.reject(400, getCompilerError(err.messages))
}
}

@@ -89,0 +94,0 @@

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

const csn = {
extensions: req.data.extensions.map(ext => JSON.parse(ext))
extensions: req.data.extensions?.map(ext => JSON.parse(ext)) ?? []
}

@@ -53,3 +53,3 @@

for (const ext of req.data.extensions) {
for (const ext of req.data.extensions ?? []) {
const extension = JSON.parse(ext)

@@ -56,0 +56,0 @@ await handleDefaults(extension, appCsn, false)

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

const PROTECTED_NAMESPACES = ['cds.xt']
class Allowlist {

@@ -59,2 +61,5 @@ constructor(mtxConfig, fullCsn) {

isAllowed(kind, name) {
// check protected internal namespaces
if (PROTECTED_NAMESPACES.some( namespace => name.startsWith(`${namespace}.`))) return false
return this.getPermission(kind, name)

@@ -61,0 +66,0 @@ }

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

// 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
// compiler validation

@@ -71,5 +83,6 @@ LOG.info(`validating extension '${tag}' ...`)

// REVISIT: Isn't that also done during activate?
const csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {}))
let csn
try {
cds.extend(csn).with(extCsn)
csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {}))
} catch (err) {

@@ -87,13 +100,2 @@ return req.reject(400, getCompilerError(err.messages))

// 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
})
LOG.info(`activating extension '${tag}' ...`)

@@ -100,0 +102,0 @@ const async = cds.context.http?.req?.headers?.prefer === 'respond-async'

@@ -24,3 +24,3 @@ const cds = require('@sap/cds/lib'), { uuid } = cds.utils

await retry(() => cds.tx({ tenant: t0 }, tx => tx.run(INSERT.into(Jobs, job))), LOG)
const jobs = clusters.map(cluster => Array.from(cluster).map(tenant => ({ job_ID, ID: uuid(), tenant, op })))
const jobs = clusters?.map(cluster => Array.from(cluster).map(tenant => ({ job_ID, ID: uuid(), tenant, op }))) ?? []
const tasks = jobs.flat()

@@ -27,0 +27,0 @@

const crypto = require('crypto')
const fs = require('fs').promises
const path = require('path')
const cds = require('@sap/cds/lib'), { tar, rimraf } = cds.utils
const cds = require('@sap/cds/lib'), { tar, rimraf, exists, read } = cds.utils
const { readData } = require('./extensibility/utils')

@@ -47,3 +47,3 @@ const conf = cds.requires['cds.xt.ModelProviderService'] || cds.requires.kinds['cds.xt.ModelProviderService']

this.before(['getCsn', 'getEdmx', 'getExtCsn'], req => {
this.before(['getCsn', 'getEdmx', 'getExtCsn', 'getI18n'], req => {
const regex = /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$|^\$hash$|^\*$/

@@ -65,12 +65,36 @@ const toggles = req.data?.toggles

// TODO save db requests, some lazy loading, abstract extension API
// - loads needed data from extensions
// - does merging
// - improve _getExtension4 ?
// - split merging
this.on('getEdmx', async req => {
const { res } = req._; if (res) res.set('Content-Type', 'application/xml')
const { service, model, locale, flavor } = req.data
const { service, model, locale, flavor, toggles, tenant } = req.data
if (service === 'all') return req.reject(501, `Edmx request for 'all' services not supported`)
let precompiledEdmx = await _getPrebuiltBundle(toggles, service, flavor, tenant)
delete req.data.flavor // we need to delete the OData 'flavor' argument, as getCsn has a different CSN `flavor` argument
const csn = model ? model : await _getCsn(req)
if (!cds.context.http || cds.context.http.res.statusCode !== 304) { // wee need to check whether the eTag handling by calling _getCsn modified the response with no modified
const edmx = cds.compile.to.edmx(csn, { service, flavor })
if (!precompiledEdmx) {
const edmx = cds.compile.to.edmx(csn, { service, flavor })
const extBundle = await _getExtI18n(req)
return locale?.at ? cds.localize(csn, locale, edmx, extBundle) : edmx
}
// localization of base model without any extra i18n
return locale?.at ? cds.localize(csn, locale, precompiledEdmx) : precompiledEdmx
}
})
this.on('getI18n', async req => {
const csn = await _getCsn(req)
if (!cds.context.http || cds.context.http.res.statusCode !== 304) {
const baseBundle = cds.localize.bundle4(csn, req.data.locale )
const extBundle = await _getExtI18n(req)
return cds.localize(csn, locale, edmx, extBundle)
return {...baseBundle, ...extBundle}
}

@@ -103,2 +127,21 @@ })

async function _getPrebuiltBundle(toggles, service, flavor, tenant) {
if (!(await _needsEdmxCompile(toggles, tenant, service))) {
const edmxPath = path.join(main.root, 'srv/odata', flavor ?? cds.env.odata.version,`${service}.xml`)
if (exists(edmxPath)) {
return read(edmxPath, 'utf-8')
}
DEBUG?.(`No precompiled bundle for ${service} found in ${edmxPath}`)
}
return null
}
async function _needsEdmxCompile(toggles, tenant, service) {
const { 'cds.xt.ModelProviderService': mp } = cds.services
if (await mp.isExtended(tenant)) {
return true
}
return !!toggles?.length
}
/** Implementation for getCsn */

@@ -193,5 +236,9 @@ const baseCache = new Map, extensionCache = new Map

let extBundle
if (extBundles && extBundles.length) {
extBundle = extBundles.reduce((acc, cur) => {
return _mergeBundles(extBundles, locale)
}
function _mergeBundles(bundles, locale) {
let mergedBundle
if (bundles && bundles.length) {
mergedBundle = bundles.reduce((acc, cur) => {
const bundle = JSON.parse(cur.i18n)

@@ -203,6 +250,5 @@ if (locale && bundle[locale]) acc[locale] = Object.assign(acc[locale] || {}, bundle[locale])

}, {})
extBundle = extBundle[locale] || extBundle['']
mergedBundle = mergedBundle[locale] || mergedBundle['']
}
return extBundle
return mergedBundle
}

@@ -209,0 +255,0 @@

@@ -20,2 +20,5 @@ 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') {

@@ -155,8 +158,4 @@

function _isExtensible() {
return cds.requires.extensibility || cds.requires['cds.xt.ExtensibilityService']
}
async function _readExtCsvs(tenant) {
if (!_isExtensible()) return
if (!isExtensible) return
const { 'cds.xt.ModelProviderService': mp } = cds.services

@@ -221,28 +220,21 @@ const extensions = await mp.getExtResources(tenant)

DEBUG?.('preparing HANA deployment artifacts')
const _resources = csnFromParameter ? null : resources4(tenant)
let container
const [_csn] = await Promise.all([
(async () => {
container = await _container // csn4 accesses tenant tables, container has to exist
return csnFromParameter ?? csn4(tenant)
})(),
(async () => {
// Note: currently the hana files are created twice, first from getResources,
// then from local compile -2 hana. This has to be adapted depending on if
// the project is extended or not. Ideally the base hana files would have to
// be filtered already when getting the resources.
let container = await _container // csn4 accesses tenant tables, container has to exist
// 1. unpack what comes from getResources()
await _resources
})()
])
// Note: currently the hana files are created twice, first from getResources,
// then from local compile -2 hana. This has to be adapted depending on if
// the project is extended or not. Ideally the base hana files would have to
// be filtered already when getting the resources.
// Can already start getting the csn if later required
const _csn = isExtensible && !csnFromParameter ? csn4(tenant) : csnFromParameter
// 1. Unpack what comes from getResources()
if (!csnFromParameter) await resources4(tenant)
// 2. Get csvs from extensions
const updateCsvs = !csnFromParameter && !!await csvs4(tenant)
// 3. run cds compile -2 hana with potentially extended model from getCsn()
// FIXME: Do that check in a better way, as ExtensibilityService is always on by new sidecar presets
const isExtensible = _isExtensible()
if (isExtensible || csnFromParameter) {
if (_csn) {
// 3. Run cds compile -2 hana with potentially extended model from getCsn()
const csn = await _csn

@@ -249,0 +241,0 @@ if (csn) await build (csn,tenant,updateCsvs)

@@ -55,22 +55,2 @@ const { join } = require('path')

const hdi_opts = _parse_env ('HDI_DEPLOY_OPTIONS', options)
const invalid = Object.keys(hdi_opts).filter (o => !(o in {
// subset of https://help.sap.com/viewer/4505d0bdaf4948449b7f7379d24d0f0d/2.0.05/en-US/a4bbc2dd8a20442387dc7b706e8d3070.html
lock_container_timeout:1,
connection_timeout:1,
write_timeout:1,
exclude_filter:1,
include_filter:1,
path_parameter:1,
working_set:1,
auto_undeploy:1,
undeploy:1,
optimise_file_upload:1,
verbose:1,
trace:1,
parameter:1,
treat_unmodified_as_modified:1
}))
if (invalid.length) {
cds.error(`HDI deployment options '${invalid}' cannot be used with MTX deployment`, { status: options ? 400 : 500 })
}
env.HDI_DEPLOY_OPTIONS = JSON.stringify (hdi_opts)

@@ -77,0 +57,0 @@ return env

@@ -29,4 +29,4 @@ const https = require('https')

} catch (e) {
if (e.response?.status === 409) service_instance_id = (await _instance4(tenant)).id
else cds.error(_errorMessage(e, 'creating', tenant), { status: e.response?.status ?? 500 })
if (e.status === 409) service_instance_id = (await _instance4(tenant)).id
else cds.error(_errorMessage(e, 'creating', tenant), { status: e.status ?? 500 })
}

@@ -87,2 +87,3 @@ const { data } = await api.post('service_bindings', {

async function _instanceName4(tenant) {
if (cds.requires.multitenancy.humanReadableInstanceName) return tenant
// Compatible with @sap/instance-manager-created instances

@@ -89,0 +90,0 @@ return require('crypto').createHash('sha256').update(`${await _planId()}_${tenant}`).digest('base64')

Sorry, the diff of this file is not supported yet

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