@sap/cds-mtxs
Advanced tools
Comparing version 1.10.0 to 1.11.0
@@ -30,2 +30,3 @@ #!/usr/bin/env node | ||
await cds.db.disconnect(cds.requires.multitenancy.t0) | ||
await cds.db.disconnect(tenant) | ||
} | ||
@@ -32,0 +33,0 @@ } |
@@ -9,2 +9,18 @@ # Change Log | ||
## Version 1.11.0 - 2023-09-01 | ||
### Added | ||
- `/-/cds/extensibility/push` now also accepts the `prefer: respond-async` header for asynchronous requests. | ||
- `/-/cds/model-provider/getCsn` uses an LRU cache for base model CSNs, limited to 5 entries by default. This cache size can be configured using `cds.requires['cds.xt.ModelProviderService'].cacheSize`. | ||
- The `treat_unmodified_as_modified` parameter is now allowed for HDI deployments. | ||
### Fixed | ||
- The Service Manager client now returns all bindings for partial cache misses for `cds.requires.['cds.xt.SaasProvisioningService'].jobs.clusterSize > 1`. | ||
- The Service Manager client will now wait with exponential back-off for unexpected error codes. | ||
- The Service Manager client will now print the correct root cause for errors with a `description` field. | ||
- Command `cds-mtx` now terminates immediately after execution is finished. | ||
## Version 1.10.0 - 2023-07-31 | ||
@@ -11,0 +27,0 @@ |
@@ -1,29 +0,27 @@ | ||
require('axios').interceptors.response.use( | ||
response => response, | ||
axError => { | ||
const { url, method } = axError.config ?? {}; | ||
const { code, response } = axError; | ||
const { status, data } = response ?? {}; | ||
const reason = data?.error /* RFC 6749 */ ?? axError.message; | ||
const prefix = (url && method | ||
? `${method.toUpperCase()} ${url} ` | ||
: '') + | ||
'failed'; | ||
const message = prefix + | ||
(status || code ? ':' : '') + | ||
(status ? ` ${status}` : '') + | ||
(code ? ` ${code}` : '') + | ||
`. ${reason}.`; | ||
const error = new Error(message); | ||
error.status = status; | ||
const cds = require('@sap/cds'); | ||
if (cds.debug('req|mtx')) { | ||
error.cause = axError; | ||
} | ||
if (data) { | ||
cds.log('req').error(`Details on error '${prefix}': ` + | ||
(data.error_description /* RFC 6749 */ || require('util').inspect(data)) + `'`); | ||
} | ||
return Promise.reject(error); | ||
module.exports = axError => { | ||
const { inspect } = require('util'); | ||
const { url, method } = axError.config ?? {}; | ||
const { code, response } = axError; | ||
const { status, data } = response ?? {}; | ||
const reason = data?.error /* RFC 6749 */ ? inspect(data.error) : axError.message; | ||
const prefix = (url && method | ||
? `${method.toUpperCase()} ${url} ` | ||
: '') + | ||
'failed'; | ||
const message = prefix + | ||
(status || code ? ':' : '') + | ||
(status ? ` ${status}` : '') + | ||
(code ? ` ${code}` : '') + | ||
`. ${reason}.`; | ||
const error = new Error(message); | ||
error.status = status; | ||
const cds = require('@sap/cds'); | ||
if (cds.debug('req|mtx')) { | ||
error.cause = axError; | ||
} | ||
); | ||
if (data) { | ||
cds.log('req').error(`Details on error '${prefix}': ` + | ||
(data.error_description /* RFC 6749 */ || inspect(data)) + `'`); | ||
} | ||
return Promise.reject(error); | ||
} |
{ | ||
"name": "@sap/cds-mtxs", | ||
"version": "1.10.0", | ||
"version": "1.11.0", | ||
"description": "SAP Cloud Application Programming Model - Multitenancy library", | ||
@@ -25,5 +25,5 @@ "homepage": "https://cap.cloud.sap/", | ||
"dependencies": { | ||
"axios": ">=0.27.2", | ||
"axios": "^1", | ||
"@sap/hdi-deploy": "^4" | ||
} | ||
} |
@@ -18,4 +18,3 @@ const cds = require('@sap/cds/lib') | ||
this.on('DELETE', 'tenant', this.delete) | ||
await super.init() // ensure to call super.init() // REVISIT: Why? | ||
await super.init() | ||
} | ||
@@ -95,3 +94,3 @@ | ||
async read(context) { // TODO check params for get | ||
async read(context) { | ||
const tenant = this._getSubscribedTenant(context) | ||
@@ -127,3 +126,3 @@ if (tenant) { | ||
const dbToTenants = clusterSize > 1 ? await this._tenantsByDb(tenants) : [new Set(tenants)] | ||
LOG.info('upgrading tenants', tenants) | ||
LOG.info('upgrading', { tenants }) | ||
if (isSync) { | ||
@@ -221,3 +220,2 @@ try { | ||
if (!isSync && callbackUrl) { | ||
/// TODO evaluate params for new rest adapter | ||
const tenant = this._getSubscribedTenant(originalRequest.body) | ||
@@ -224,0 +222,0 @@ const payload = { status, message, subscriptionUrl } |
const https = require('https') | ||
const { URL } = require('url') | ||
const axios = require('axios') | ||
const cds = require('@sap/cds/lib') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
require('../../lib/pruneAxiosErrors') | ||
const axiosInstance = require('axios').create(); | ||
axiosInstance.interceptors.response.use(response => response, require('../../lib/pruneAxiosErrors')) | ||
@@ -31,5 +31,5 @@ module.exports = new class SaasRegistryUtil { | ||
try { | ||
return await axios(callbackUrl, { method: 'PUT', headers, data }) | ||
return await axiosInstance(callbackUrl, { method: 'PUT', headers, data }) | ||
} catch (error) { | ||
cds.error('Error sending result callback to saas-registry: ' + error.message) // REVISIT: Just throw error? | ||
cds.error('Error sending result callback to saas-registry: ' + error.message) | ||
} | ||
@@ -64,3 +64,3 @@ } | ||
LOG.info(`getting saas-registry auth token from ${authUrl}`) | ||
const { data: { access_token } } = await axios(authUrl, { | ||
const { data: { access_token } } = await axiosInstance(authUrl, { | ||
method: 'POST', | ||
@@ -67,0 +67,0 @@ ...auth, |
@@ -16,10 +16,15 @@ const cds = require('@sap/cds/lib') | ||
const activate = async function ExtensibilityService_activate (ID, tag, tenant, csvs) { | ||
const activate = async function ExtensibilityService_activate (ID, tag, tenant, csvs, async) { | ||
if (tenant) cds.context = { tenant } | ||
await _updateExtensions(ID, tag) | ||
const { 'cds.xt.DeploymentService': ds } = cds.services | ||
await ds.extend(tenant, csvs) | ||
if (async) { | ||
const { 'cds.xt.JobsService': js } = cds.services | ||
return js.enqueue([new Set([tenant])], 'extend', [csvs]) | ||
} else { | ||
const { 'cds.xt.DeploymentService': ds } = cds.services | ||
await ds.extend(tenant, csvs) | ||
} | ||
} | ||
module.exports = activate |
const config = require('./config') | ||
const LinterMessage = require('../linter/linterMessage') | ||
const LinterMessage = require('../linter/message') | ||
@@ -54,7 +54,7 @@ const parse_options = { | ||
const ast = acorn.parse(code, parse_options) | ||
walk.full(ast, node => { | ||
walk.full(ast, node => { | ||
if (config.restrict.language_keywords.includes(node.type)) { | ||
findings.push(new LinterMessage(`${file} (${node.start}-${node.end}): Includes a forbidden construct ${node.type}`, { $location: { file } })) | ||
} | ||
config.restrict.language_concepts.forEach(concept => { | ||
@@ -65,3 +65,3 @@ const finding = concepts[concept](node, file) | ||
} | ||
}) | ||
}) | ||
@@ -68,0 +68,0 @@ if (config.restrict.globals.includes(node.name)) { |
const cds = require('@sap/cds/lib') | ||
const { set_ } = require('./add') | ||
const TOMBSTONE_ID = '__tombstone' | ||
const readExtension = async function (req) { | ||
@@ -9,3 +11,3 @@ const tenant = _tenant(req) | ||
const ext = !req.data?.ID ? await SELECT.from('cds.xt.Extensions') : await SELECT.one.from('cds.xt.Extensions').where({ tag: req.data.ID }) | ||
if (Array.isArray(ext)) return ext.map(item => ({ ID: item.tag, csn: item.csn, timestamp: item.timestamp })) | ||
if (Array.isArray(ext)) return ext.filter(item => item.tag !== TOMBSTONE_ID).map(item => ({ ID: item.tag, csn: item.csn, timestamp: item.timestamp })) | ||
if (ext) return { ID: ext.tag, csn: ext.csn, timestamp: ext.timestamp } | ||
@@ -27,3 +29,8 @@ return ext | ||
return !req.data?.ID ? DELETE.from('cds.xt.Extensions') : DELETE.from('cds.xt.Extensions').where({ tag: req.data.ID }) | ||
const result = !req.data?.ID ? await DELETE.from('cds.xt.Extensions') : await DELETE.from('cds.xt.Extensions').where({ tag: req.data.ID }) | ||
// 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 }) | ||
return result | ||
} | ||
@@ -30,0 +37,0 @@ |
const cds = require('@sap/cds/lib') | ||
const { deduplicateMessages } = require('@sap/cds-compiler') | ||
const NamespaceChecker = require('./namespace_checker') | ||
const AnnotationsChecker = require('./annotations_checker') | ||
const AllowlistChecker = require('./allowlist_checker') | ||
const CodeChecker = require('./code-checker') | ||
const NamespaceChecker = require('./namespace') | ||
const AnnotationsChecker = require('./annotations') | ||
const AllowlistChecker = require('./allowlist') | ||
const CodeChecker = require('./code') | ||
@@ -9,0 +9,0 @@ const LINTER_OPTIONS = ['element-prefix', 'extension-allowlist', 'namespace-blocklist'] |
@@ -17,4 +17,4 @@ const cds = require('@sap/cds/lib'), { fs, path, tar, rimraf } = cds.utils | ||
try { | ||
const { extCsn, bundles, csvs } = await readData(extension, root) | ||
const { extCsn, bundles, csvs } = await readData(extension, root) | ||
if (main.requires.extensibility?.code) await addCodeAnnotations(root, extCsn, req.tenant) | ||
@@ -79,3 +79,3 @@ | ||
// extension linters | ||
const findings = linter.lint(extCsn, csn, cds.env) | ||
const findings = linter.lint(extCsn, csn) | ||
if (findings.length > 0) { | ||
@@ -99,5 +99,6 @@ let message = `Validation for ${tag} failed with ${findings.length} finding(s):\n\n` | ||
LOG.info(`activating extension '${tag}' ...`) | ||
await activate(ID, null, tenant, csvs) | ||
const async = cds.context.http?.req?.headers?.prefer === 'respond-async' | ||
await activate(ID, null, tenant, csvs, async) | ||
} | ||
module.exports = { push, pull } |
@@ -6,3 +6,4 @@ const cds = require('@sap/cds/lib'); | ||
const DEBUG = cds.debug('req|mtx'); | ||
require('../../../lib/pruneAxiosErrors'); | ||
const axiosInstance = require('axios').create(); | ||
axiosInstance.interceptors.response.use(response => response, require('../../../lib/pruneAxiosErrors')); | ||
@@ -46,3 +47,3 @@ async function parseBody(request) { | ||
try { | ||
const { data } = await require('axios').post( | ||
const { data } = await axiosInstance.post( | ||
authProvider.authUrl, | ||
@@ -49,0 +50,0 @@ authProvider.postData, |
@@ -5,11 +5,11 @@ const { path, tar, read, readdir } = cds.utils | ||
await tar.xvz(extension).to(root) | ||
let extCsn = {} | ||
try { extCsn = JSON.parse(await read(path.join(root, 'extension.csn'))) } | ||
catch(e) { if (e.code !== 'ENOENT') throw e } | ||
let bundles | ||
try { bundles = await read(path.join(root, 'i18n', 'i18n.json')) } | ||
catch(e) { if (e.code !== 'ENOENT') throw e } | ||
let csvs = {} | ||
@@ -24,3 +24,3 @@ try { | ||
catch(e) { if (e.code !== 'ENOENT') throw e } | ||
return { extCsn, bundles, csvs } | ||
@@ -27,0 +27,0 @@ } |
@@ -0,1 +1,2 @@ | ||
const crypto = require('crypto') | ||
const fs = require('fs').promises | ||
@@ -101,2 +102,3 @@ const path = require('path') | ||
/** Implementation for getCsn */ | ||
const baseCache = new Map, extensionCache = new Map | ||
async function _getCsn (req, checkExt) { | ||
@@ -122,5 +124,11 @@ const { tenant, toggles, base, flavor, for:javaornode, activated } = req.data | ||
DEBUG?.('loading models for', { tenant, toggles } ,'from', models.map (cds.utils.local)) | ||
let csn = await cds.load (models, { flavor, silent:true }) | ||
if (csn.meta?.flavor === 'inferred') csn = cds.minify (csn) | ||
if (extensions) csn = cds.extend (csn) .with (extensions) | ||
let csn = await lru4(baseCache, JSON.stringify({ models, flavor }), async () => { | ||
const csn = await cds.load(models, { flavor, silent:true }) | ||
return csn.meta?.flavor === 'inferred' ? cds.minify(csn) : csn | ||
}) | ||
if (extensions) { | ||
const key = crypto.createHash('sha256').update(JSON.stringify({ extensions, models })).digest('hex') | ||
csn = lru4(extensionCache, key, () => cds.extend (csn) .with (extensions)) | ||
} | ||
if (javaornode) csn = cds.compile.for[javaornode] (csn) | ||
@@ -133,3 +141,25 @@ | ||
/** Implementation for getExtensions */ | ||
/** | ||
* A Least Recently Used (LRU) cache. | ||
* @template T | ||
* @param {string} key - Unique string for cache look-up. | ||
* @param {() => T} fn - Function producing result to be cached. | ||
* @returns {T} | ||
*/ | ||
function lru4(cache, key, fn) { | ||
// Starting simple, might later have different/dynamic cache sizes | ||
const cacheSize = cds.requires['cds.xt.ModelProviderService']?.cacheSize ?? 5 | ||
let _csn = cache.get(key) | ||
if (_csn) { | ||
cache.delete(key) | ||
cache.set(key, _csn) | ||
} else { | ||
cache.set(key, _csn = fn()) | ||
if (cache.size > cacheSize) { | ||
cache.delete(cache.keys().next().value) | ||
} | ||
} | ||
return _csn | ||
} | ||
async function _getExtensions4 (tenant, activated = false) { | ||
@@ -151,3 +181,3 @@ if (!main.requires.extensibility || !tenant && main.requires.multitenancy) return | ||
} catch (error) { | ||
DEBUG?.('cds.xt.Extensions not yet deployed', error) // REVISIT: Questionable usage of try-catch pattern | ||
DEBUG?.(`cds.xt.Extensions not yet deployed for tenant ${tenant}`, error) // REVISIT: Questionable usage of try-catch pattern | ||
} | ||
@@ -187,3 +217,3 @@ } | ||
} catch (error) { | ||
DEBUG?.('cds.xt.Extensions not yet deployed', error) // REVISIT: Questionable usage of try-catch pattern | ||
DEBUG?.(`cds.xt.Extensions not yet deployed for tenant ${tenant}`, error) // REVISIT: Questionable usage of try-catch pattern | ||
return BASE_MODEL_ETAG | ||
@@ -216,3 +246,3 @@ } | ||
} catch (e) { | ||
DEBUG?.('cds.xt.Extensions not yet deployed', e) // REVISIT: Questionable usage of try-catch pattern | ||
DEBUG?.(`cds.xt.Extensions not yet deployed for tenant ${tenant}`, e) // REVISIT: Questionable usage of try-catch pattern | ||
return null | ||
@@ -219,0 +249,0 @@ } |
@@ -5,3 +5,3 @@ const cds = require('@sap/cds/lib') | ||
exports.activated = 'Generic metadata' | ||
exports.activated = 'Generic Metadata' | ||
@@ -8,0 +8,0 @@ // Add database-agnostic metadata handlers to DeploymentService... |
@@ -247,3 +247,3 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires | ||
if (csn) await build (csn,tenant,updateCsvs) | ||
DEBUG?.('HANA build successfully finished') | ||
DEBUG?.('finished HANA build') | ||
} | ||
@@ -250,0 +250,0 @@ if (csnFromParameter) { |
@@ -10,3 +10,3 @@ const { join } = require('path') | ||
DEBUG?.(`deployment directory: ${cwd}`) | ||
DEBUG?.(`effective HDI options: ${env.HDI_DEPLOY_OPTIONS}`) | ||
DEBUG?.(`effective HDI options:`, env.HDI_DEPLOY_OPTIONS) | ||
@@ -70,3 +70,4 @@ const logPath = join(cds.root, 'logs', `${cds.context.tenant}.log`) | ||
trace:1, | ||
parameter:1 | ||
parameter:1, | ||
treat_unmodified_as_modified:1 | ||
})) | ||
@@ -73,0 +74,0 @@ if (invalid.length) { |
const https = require('https') | ||
const cds = require('@sap/cds') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx|sm') | ||
const { uuid } = cds.utils | ||
@@ -8,4 +8,3 @@ const { cacheBindings = true } = cds.env.requires.multitenancy ?? {} | ||
const axios = require('axios') | ||
require('../../../lib/pruneAxiosErrors') | ||
const api = axios.create({ baseURL: sm_url + '/v1/', 'Content-Type': 'application/json' }) | ||
const api = axios.create({ baseURL: sm_url + '/v1/', headers: { 'Content-Type': 'application/json' }}) | ||
api.interceptors.request.use(async config => { | ||
@@ -15,2 +14,3 @@ config.headers.Authorization = await _token() | ||
}) | ||
api.interceptors.response.use(response => response, require('../../../lib/pruneAxiosErrors')) | ||
@@ -94,4 +94,5 @@ /* API */ | ||
async function _bindings4(tenants, options = {}) { | ||
const uncached = cacheBindings && !options.disableCache && _bindings4.cached && tenants !== '*' ? tenants.filter(t => !(t in _bindings4.cached)) : tenants | ||
DEBUG && DEBUG('Retrieving', { tenants }, { uncached }) | ||
const useCache = cacheBindings && !options.disableCache && tenants !== '*' | ||
const uncached = useCache ? tenants.filter(t => !(t in _bindings4.cached)) : tenants | ||
DEBUG?.('retrieving', { tenants }, { uncached }) | ||
if (uncached.length === 0) return tenants.map(t => _bindings4.cached[t]) | ||
@@ -101,13 +102,15 @@ const _tenantFilter = () => ` and tenant_id in (${uncached.map(t => `'${t}'`).join(', ')})` | ||
const labelQuery = `service_plan_id eq '${await _planId()}'` + tenantFilter | ||
const bindings = []; let token | ||
const fetched = []; let token | ||
do { | ||
// eslint-disable-next-line no-await-in-loop | ||
const { items, token: nextPageToken } = (await api.get('service_bindings', { params: { token, labelQuery }})).data | ||
bindings.push(...items) | ||
fetched.push(...items) | ||
token = nextPageToken | ||
} while (token) | ||
if (cacheBindings) Object.assign(_bindings4.cached ?? {}, | ||
Object.fromEntries(bindings.filter(b => b.labels?.tenant_id).map(b => [b.labels.tenant_id[0], b])) | ||
) | ||
return bindings | ||
if (useCache) { | ||
const cacheMisses = Object.fromEntries(fetched.filter(b => b.labels?.tenant_id).map(b => [b.labels.tenant_id[0], b])) | ||
Object.assign(_bindings4.cached, cacheMisses) | ||
return tenants.map(t => _bindings4.cached[t]) | ||
} | ||
return fetched | ||
} | ||
@@ -160,3 +163,3 @@ | ||
const msg = `Error ${action} tenant ${tenant}: ${e.response?.data?.error ?? e.code ?? 'unknown error'}` | ||
const cause = e.cause ? require('os').EOL + `Root Cause: ${e.description ?? e.cause}` : '' | ||
const cause = e.description || e.cause ? require('os').EOL + `Root Cause: ${e.description ?? e.cause}` : '' | ||
return msg + cause | ||
@@ -181,3 +184,3 @@ } | ||
delay = 300 * 2 ** (attempt - 1) | ||
} else if (status in { 500: 1, 503: 1 }) { | ||
} else { | ||
delay = 1000 * 3 ** (attempt - 1) | ||
@@ -184,0 +187,0 @@ } |
Sorry, the diff of this file is not supported yet
222265
4324
Updatedaxios@^1