@sap/cds-mtxs
Advanced tools
Comparing version 1.18.2 to 2.0.2
@@ -9,2 +9,43 @@ # Change Log | ||
## Version 2.0.2 - 2024-07-11 | ||
### Changed | ||
- Calls to Service Manager now include the `Client-ID` and `Client-Version` headers. | ||
### Fixed | ||
- Reduced number of Service Manager requests to `service_instances` in the error case. | ||
## Version 2.0.1 - 2024-07-04 | ||
### Added | ||
- Added HANA build plugin mappings `.hdbeshconfig` and `.hdbcalculationview` required for enterprise search and embedded analytics integration. | ||
- The Service Manager client reports the container state on timeouts. | ||
### Fixed | ||
- Improved robustness in case of temporary extension inconsistencies. | ||
- The Service Manager client now stores instance and binding locations used for async polling in-memory, allowing parallel subscriptions for single-instance applications. | ||
- The Service Manager client automatically recreates instances in "creation failed" state on subscription. | ||
### Removed | ||
- `@sap/instance-manager` is not supported any longer as a fallback to the built-in Service Manager client. | ||
## Version 2.0.0 - 2024-06-19 | ||
### Added | ||
- Additional endpoint to get the passcode URL. | ||
- Dependencies for the SAP BTP Connectivity, Audit Logging, and Destinations services are automatically added if `cds.requires.[connectivity|audit-log|destinations]` properties are set, respectively. | ||
### Changed | ||
- `@sap/cds-mtxs` now requires `@sap/hdi-deploy >= 4`. | ||
- Deprecated endpoint `upgradeAll` has been removed from `SaasProvisioningService`. | ||
- Use the `cds.compile.to.hana` API to support cds plugins such as embedded analytics. | ||
- When pushing an extension, the extension is blocked if it contains critical annotations. | ||
## Version 1.18.2 - 2024-06-24 | ||
@@ -22,2 +63,3 @@ | ||
## Version 1.18.1 - 2024-05-16 | ||
@@ -62,2 +104,3 @@ | ||
- `/-/cds/saas-provisioning/upgrade` sent as an async request with payload `"tenants": ["*"]` will now return job information even if no tenants are found. | ||
- Default restrictions for code extensions and security annotations are now aways applied. | ||
@@ -64,0 +107,0 @@ ### Fixed |
27
env.js
@@ -0,1 +1,4 @@ | ||
const path = require('path') | ||
const cds = require('@sap/cds') | ||
const hana_mt = { kind: 'hana', | ||
@@ -54,2 +57,22 @@ "deploy-format": "hdbtable", | ||
}, | ||
"audit-log": { | ||
vcap: { label: "auditlog" }, | ||
subscriptionDependency: { | ||
uaa: 'xsappname' | ||
} | ||
}, | ||
"portal": { | ||
vcap: { label: "portal" }, | ||
subscriptionDependency: { | ||
uaa: 'xsappname' | ||
} | ||
}, | ||
"connectivity": { | ||
vcap: { label: "connectivity" }, | ||
subscriptionDependency: 'xsappname' | ||
}, | ||
"destinations": { | ||
vcap: { label: "destination" }, | ||
subscriptionDependency: 'xsappname' | ||
}, | ||
@@ -91,3 +114,5 @@ //////////////////////////////////////////////////////////////////////// | ||
"[mtx-sidecar]": { | ||
i18n: { | ||
"[development]": { root: path.resolve(cds.root, '../..') } | ||
}, | ||
requires: { | ||
@@ -94,0 +119,0 @@ db: { |
@@ -5,90 +5,2 @@ const cds = require('@sap/cds') | ||
// REVISIT: eliminate usage of these helpers | ||
const { ensureNoDraftsSuffix, ensureUnlocalized } = require('@sap/cds/libx/_runtime/common/utils/draft') | ||
const EXT_BACK_PACK = 'extensions__' | ||
const getTargetRead = req => { | ||
let name = '' | ||
if (req.query.SELECT.from.join && req.query.SELECT.from.args) { | ||
// join | ||
name = req.query.SELECT.from.args.find(arg => arg.ref && arg.ref[0] !== 'DRAFT.DraftAdministrativeData').ref[0] | ||
} else if (req.target.name.SET) { | ||
// union | ||
name = req.target.name.SET.args[0]._target.name | ||
} else { | ||
// simple select | ||
name = req.target.name | ||
} | ||
return { name: ensureUnlocalized(ensureNoDraftsSuffix(name)) } | ||
} | ||
const getTargetWrite = (target, model) => { | ||
return model.definitions[ensureUnlocalized(ensureNoDraftsSuffix(target.name))] | ||
} | ||
const isExtendedEntity = (entityName, model) => { | ||
const entity = model.definitions[ensureUnlocalized(ensureNoDraftsSuffix(entityName))] | ||
if (!entity) return false | ||
return entity.elements[EXT_BACK_PACK] || Object.values(entity.elements).some(el => el['@cds.extension']) | ||
} | ||
const _hasExtendedEntityArgs = (args, model) => { | ||
return args.find(arg => { | ||
if (arg.ref) { | ||
return arg.ref[0] !== 'DRAFT.DraftAdministrativeData' && isExtendedEntity(arg.ref[0], model) | ||
} | ||
if (arg.join) { | ||
return _hasExtendedEntityArgs(arg.args, model) | ||
} | ||
}) | ||
} | ||
const _hasExtendedExpand = (columns, targetName, model) => { | ||
for (const col of columns) { | ||
if (col.ref && col.expand) { | ||
let targetNameModel = ensureUnlocalized(ensureNoDraftsSuffix(targetName)) | ||
if (cds.env.fiori?.lean_draft) targetNameModel = targetNameModel.replace(/\.drafts$/,'') | ||
const expTargetName = model.definitions[targetNameModel].elements[col.ref[col.ref.length - 1]].target | ||
if (isExtendedEntity(expTargetName, model)) return true | ||
_hasExtendedExpand(col.expand, expTargetName, model) | ||
} | ||
} | ||
} | ||
const hasExtendedEntity = (req, model) => { | ||
if (!req.query.SELECT) return false | ||
if (req.query.SELECT.columns && req.target && _hasExtendedExpand(req.query.SELECT.columns, req.target.name, model)) { | ||
return true | ||
} | ||
if (req.query.SELECT.from.join && req.query.SELECT.from.args) { | ||
return _hasExtendedEntityArgs(req.query.SELECT.from.args, model) | ||
} | ||
if (req.target) { | ||
if (req.target.name.SET) { | ||
return isExtendedEntity(req.target.name.SET.args[0]._target.name, model) | ||
} | ||
return isExtendedEntity(req.target.name, model) | ||
} | ||
} | ||
const getExtendedFields = (entityName, model) => { | ||
const elements = model.definitions[ensureUnlocalized(ensureNoDraftsSuffix(entityName))].elements | ||
return Object.values(elements) | ||
.filter(element => { | ||
return element['@cds.extension'] | ||
}) | ||
.map(element => { | ||
return element.name | ||
}) | ||
} | ||
const getCompilerError = messages => { | ||
@@ -148,8 +60,2 @@ const defaultMsg = 'Error while compiling extension' | ||
module.exports = { | ||
EXT_BACK_PACK, | ||
getTargetRead, | ||
getTargetWrite, | ||
isExtendedEntity, | ||
hasExtendedEntity, | ||
getExtendedFields, | ||
getCompilerError, | ||
@@ -156,0 +62,0 @@ t0_, |
{ | ||
"name": "@sap/cds-mtxs", | ||
"version": "1.18.2", | ||
"version": "2.0.2", | ||
"description": "SAP Cloud Application Programming Model - Multitenancy library", | ||
@@ -25,5 +25,5 @@ "homepage": "https://cap.cloud.sap/", | ||
"dependencies": { | ||
"@sap/hdi-deploy": "^4", | ||
"@sap/hdi-deploy": ">=4", | ||
"axios": "^1" | ||
} | ||
} |
@@ -60,3 +60,3 @@ const cds = require('@sap/cds/lib') | ||
const { headers, data, http } = context | ||
DEBUG?.('received subscription request with', { data }) | ||
DEBUG?.('received subscription request with', { data: require('util').inspect(data, { depth: 11 }) }) | ||
const options = this._options4(metadata) | ||
@@ -215,10 +215,30 @@ // REVISIT: removing as they are polluting logs -> clearer data/options separation | ||
async _upgradeAll(context) { | ||
const { tenants } = context.data | ||
LOG.warn(`upgradeAll is deprecated. Use /-/cds/saas-provisioning/upgrade instead.`) | ||
return this.__upgrade(tenants ?? ['*']) | ||
} | ||
_dependencies() { | ||
return cds.env.requires[this.name]?.dependencies?.map(d => ({ xsappname: d })) ?? [] | ||
// Compat for cds.requires.multitenancy.dependencies | ||
const provisioning = cds.env.requires[this.name] ?? cds.env.requires.multitenancy | ||
if (provisioning?.dependencies) { | ||
return provisioning?.dependencies?.map(d => ({ xsappname: d })) ?? [] | ||
} | ||
// Construct from cds.requires | ||
let dependencies = [] | ||
const extractDependency = (node, root) => { | ||
if (typeof node === 'object' && node !== null) { | ||
Object.entries(node).forEach(([key, value]) => { | ||
if (typeof value === 'object') { | ||
extractDependency(value, root[key]) | ||
} else { | ||
dependencies.push(root[key][value]) | ||
} | ||
}) | ||
} else if (typeof node === 'string') { | ||
dependencies.push(root[node]) | ||
} | ||
} | ||
Object.entries(cds.env.requires).forEach(([name, req]) => { | ||
const tree = req.subscriptionDependency | ||
if (!tree) return | ||
extractDependency(tree, cds.requires[name].credentials) | ||
}) | ||
LOG.info('using SaaS dependencies', dependencies) | ||
return dependencies.map(d => ({ xsappname: d })) | ||
} | ||
@@ -225,0 +245,0 @@ |
@@ -16,3 +16,2 @@ const cds = require('@sap/cds/lib') | ||
this.on('upgrade', super._upgrade) | ||
this.on('upgradeAll', super._upgradeAll) | ||
await super.init() | ||
@@ -19,0 +18,0 @@ } |
const cds = require('@sap/cds/lib') | ||
const main = require('./config') | ||
const addExtension = require('./extensibility/addExtension') | ||
const { add, promote, setExtension } = require('./extensibility/add') | ||
const { setExtension } = require('./extensibility/set') | ||
const { push, pull } = require('./extensibility/push') | ||
const { readExtension, updateExtension, deleteExtension } = require('./extensibility/crud') | ||
const token = require('./extensibility/token') | ||
const { transformExtendedFieldsCREATE, transformExtendedFieldsUPDATE } = require('./extensibility/handler/transformWRITE') | ||
const { transformExtendedFieldsREAD } = require('./extensibility/handler/transformREAD') | ||
const { transformExtendedFieldsRESULT } = require('./extensibility/handler/transformRESULT') | ||
const { token, authMeta } = require('./extensibility/token') | ||
const { addCodeAnnotations } = require('./extensibility/code-extensibility/addCodeAnnotLocal') | ||
@@ -18,6 +14,3 @@ | ||
async init() { | ||
this.on('addExtension', addExtension) | ||
this.on('add', add) | ||
this.on('promote', promote) | ||
async init() { | ||
this.on('push', push) | ||
@@ -54,16 +47,8 @@ this.on('pull', pull) | ||
cds.app.post('/-/cds/login/token', token) | ||
cds.app.get('/-/cds/login/token', token) | ||
cds.app.get('/-/cds/login/authorization-metadata', authMeta) | ||
} | ||
}) | ||
}) | ||
const { 'cds.xt.ModelProviderService': mps } = cds.services | ||
if (!mps?._in_sidecar && cds.db?.model) | ||
cds.db | ||
.before('CREATE', transformExtendedFieldsCREATE) | ||
.before('UPDATE', transformExtendedFieldsUPDATE) | ||
.before('READ', transformExtendedFieldsREAD) | ||
.after('READ', transformExtendedFieldsRESULT) | ||
return super.init() | ||
} | ||
} |
const cds = require('@sap/cds/lib') | ||
const Extensions = 'cds.xt.Extensions' | ||
const _updateExtensions = async function (ID, tag) { | ||
const updateCqn = UPDATE(Extensions) | ||
.with(`csn = REPLACE(csn, ',"@cds.extension":true', ''), activated = 'database'`) | ||
.where({ activated: 'propertyBag' }) | ||
if (ID) { | ||
updateCqn.where('ID =', ID) | ||
} else if (tag) { | ||
updateCqn.where('tag =', tag) | ||
} | ||
await cds.db.run(updateCqn) | ||
} | ||
const activate = async function ExtensibilityService_activate (ID, tag, tenant, csvs, async) { | ||
const activate = async function (tenant, csvs, async) { | ||
if (tenant) cds.context = { tenant } | ||
await _updateExtensions(ID, tag) | ||
@@ -20,0 +6,0 @@ if (async) { |
const cds = require('@sap/cds/lib') | ||
const { set_ } = require('./add') | ||
const { set_ } = require('./set') | ||
@@ -39,3 +39,3 @@ const TOMBSTONE_ID = '__tombstone' | ||
if (!req.user.is('internal-user') && tenant && tenant !== req.tenant) | ||
req.reject(403, `No permission to promote extensions by tenants other than ${req.tenant}`) | ||
req.reject(403, `No permission to activate extensions by tenants other than ${req.tenant}`) | ||
return tenant | ||
@@ -42,0 +42,0 @@ } |
const LinterMessage = require('./message') | ||
const AT_REQUIRES = '@requires' | ||
const AT_RESTRICT = '@restrict' | ||
const AT_CDS_PERSISTENCE_JOURNAL = '@cds.persistence.journal' | ||
const AT_SQL_APPEND = '@sql.append' | ||
const AT_SQL_PREPEND = '@sql.prepend' | ||
const checkedAnnotations = new Map([ | ||
[AT_REQUIRES, _createSecurityAnnotationWarning], | ||
[AT_RESTRICT, _createSecurityAnnotationWarning], | ||
[AT_CDS_PERSISTENCE_JOURNAL, _createJournalAnnotationWarning], | ||
[AT_SQL_APPEND, _createSqlAnnotationWarning], | ||
[AT_SQL_PREPEND, _createSqlAnnotationWarning] | ||
// annotations with specific checks or messages | ||
const checkedExtensionAnnotations = new Map([ | ||
['@requires', _createSecurityAnnotationWarning], | ||
['@restrict', _createSecurityAnnotationWarning], | ||
['@cds.persistence.journal', _createJournalAnnotationWarning], | ||
['@mandatory', _createMandatoryAnnotationWarning], | ||
['@assert.notNull', _createMandatoryAnnotationWarning], | ||
['@assert.range', _createMandatoryAnnotationWarning] | ||
]) | ||
function _createSqlAnnotationWarning(annotationName, annoOrElem) { | ||
const criticalNewEntityAnnotations = /@sql.prepend|@sql.append|@cds.persistence.(?!skip)\w*/ | ||
const criticalExtensionAnnotations = new RegExp('^@(?:' | ||
+ 'requires' | ||
+ '|restrict' | ||
+ '|readonly' | ||
+ '|mandatory' | ||
+ '|assert.*' | ||
+ '|cds.persistence.*' | ||
+ '|sql.append' | ||
+ '|sql.prepend' | ||
// service annotations | ||
+ '|path' | ||
+ '|impl' | ||
+ '|cds.autoexpose' | ||
+ '|cds.api.ignore' | ||
+ '|odata.etag' | ||
+ '|cds.query.limit' | ||
+ '|cds.localized' | ||
+ '|cds.valid.*' | ||
+ '|cds.search)' | ||
); | ||
function locationString(element) { | ||
const loc = element?.$location | ||
if (!loc) return '' | ||
const line = loc.line ? `${loc.line}:` : '' | ||
return loc.col ? `${loc.file}:${line}${loc.col}:` : `${loc.file}:${line}` | ||
} | ||
function _createGenericAnnotationWarning(annotationName, annoOrElem) { | ||
const message = `Annotation '${annotationName}' in '${ | ||
annoOrElem.annotate || annoOrElem.name | ||
annoOrElem.element.annotate || annoOrElem.element.name || annoOrElem.parent.extend || annoOrElem.parent.annotate | ||
}' is not supported in extensions` | ||
return new LinterMessage(message, annoOrElem) | ||
return new LinterMessage(locationString(annoOrElem.element) + message, annoOrElem.element) | ||
} | ||
function _createMandatoryAnnotationWarning(annotationName, annoOrElem) { | ||
if (annoOrElem.element.default) return | ||
const message = `Annotation '${annotationName}' in '${ | ||
annoOrElem.element.annotate || annoOrElem.element.name || annoOrElem.parent.extend || annoOrElem.parent.annotate | ||
}' is not supported in extensions without default value` | ||
return new LinterMessage(locationString(annoOrElem.element) + message, annoOrElem.element) | ||
} | ||
function _createSecurityAnnotationWarning(annotationName, annoOrElem) { | ||
const message = `Security relevant annotation '${annotationName}' in '${ | ||
annoOrElem.annotate || annoOrElem.name | ||
annoOrElem.element.annotate || annoOrElem.element.name | ||
}' cannot be overwritten` | ||
return new LinterMessage(message, annoOrElem) | ||
return new LinterMessage(locationString(annoOrElem.element) + message, annoOrElem.element) | ||
} | ||
@@ -33,5 +66,5 @@ | ||
const message = `Enabling schema evolution in extensions using '${annotationName}' in '${ | ||
annoOrElem.annotate || annoOrElem.name | ||
}' not yet supported` | ||
return new LinterMessage(message, annoOrElem) | ||
annoOrElem.element.annotate || annoOrElem.element.name | ||
}' not supported` | ||
return new LinterMessage(locationString(annoOrElem.element) + message, annoOrElem.element) | ||
} | ||
@@ -41,3 +74,3 @@ | ||
const message = `Extending entity '${element.extend}' is not supported as the corresponding database table has been enabled for schema evolution` | ||
return new LinterMessage(message, element) | ||
return new LinterMessage(locationString(element) + message, element) | ||
} | ||
@@ -51,6 +84,3 @@ | ||
// annotations via annotate - applies for all | ||
const annotationExtensions = Object.values(reflectedCsn.extensions).filter( | ||
value => value.annotate && Object.getOwnPropertyNames(value).filter(property => checkedAnnotations.get(property)) | ||
) | ||
const annotationExtensions = [] | ||
const messages = [] | ||
@@ -61,11 +91,10 @@ | ||
() => true, | ||
element => { | ||
if (element[AT_SQL_PREPEND] || element[AT_SQL_APPEND]) { | ||
if (!element.annotate) { | ||
// do not add annotation extensions again | ||
annotationExtensions.push(element) | ||
(element, name, parent) => { | ||
if (Object.getOwnPropertyNames(element).filter(property => | ||
property.startsWith('@') && criticalExtensionAnnotations.test(property)).length) { | ||
annotationExtensions.push({element, name, parent}) | ||
} | ||
} | ||
if (element.extend) { | ||
this._checkExtendedEntityAnnotations(fullCsn, element, messages) | ||
// check base entity for incompatible annotations | ||
this._checkExtendedEntityAnnotations(fullCsn, element, messages) // checks e. g. for journal annotations in base entity | ||
} | ||
@@ -76,9 +105,9 @@ }, | ||
// check entities and fields from definitions | ||
// check entities and fields from new definitions | ||
const annotatedDefinitions = [] | ||
reflectedCsn.forall( | ||
() => true, | ||
element => { | ||
if (element[AT_SQL_PREPEND] || element[AT_SQL_APPEND] || element[AT_CDS_PERSISTENCE_JOURNAL]) { | ||
annotatedDefinitions.push(element) | ||
(element, name, parent) => { | ||
if (Object.getOwnPropertyNames(element).filter(property => criticalNewEntityAnnotations.test(property)).length) { | ||
annotatedDefinitions.push({element, name, parent}) | ||
} | ||
@@ -89,4 +118,4 @@ }, | ||
for (const annotationExtension of annotationExtensions) { | ||
const warning = this._checkAnnotation(annotationExtension, reflectedCsn.definitions, compileDir) | ||
for (const annotation of [...annotationExtensions, ...annotatedDefinitions]) { | ||
const warning = this._checkExtensionAnnotation(annotation, reflectedCsn.definitions, compileDir) | ||
if (warning) { | ||
@@ -97,32 +126,19 @@ messages.push(warning) | ||
for (const annotatedDefinition of annotatedDefinitions) { | ||
const warning = this._checkAnnotation(annotatedDefinition, reflectedCsn.definitions, compileDir) | ||
if (warning) { | ||
messages.push(warning) | ||
} | ||
} | ||
return messages | ||
} | ||
_checkAnnotation(annotation, definitions, compileDir) { | ||
if (!definitions[annotation.annotate]) { | ||
return this._createAnnotationsWarning(annotation, compileDir) | ||
_checkExtensionAnnotation(annotation, definitions) { | ||
if (!definitions[annotation.element.annotate ?? annotation.parent?.annotate ?? annotation.parent?.extend]) { | ||
const annotationName = Object.getOwnPropertyNames(annotation.element).filter(property => property.startsWith('@')) | ||
if (annotationName.length) { | ||
const fn = checkedExtensionAnnotations.get(annotationName[0]) ?? _createGenericAnnotationWarning | ||
return fn(annotationName, annotation) | ||
} | ||
} | ||
return null | ||
} | ||
_createAnnotationsWarning(annotation) { | ||
const annotationName = Object.getOwnPropertyNames(annotation).filter(property => checkedAnnotations.get(property)) | ||
if (annotationName.length) { | ||
const fn = checkedAnnotations.get(annotationName[0]).bind(this) | ||
return fn(annotationName, annotation) | ||
} | ||
} | ||
_checkExtendedEntityAnnotations(fullCsn, element, messages) { | ||
const kind = fullCsn.definitions[element.extend]?.kind | ||
if (kind === 'entity' && fullCsn.definitions[element.extend]?.[AT_CDS_PERSISTENCE_JOURNAL]) { | ||
if (kind === 'entity' && fullCsn.definitions[element.extend]?.['@cds.persistence.journal']) { | ||
messages.push(_createJournalEntityExtensionNotAllowedWarning(element)) | ||
@@ -129,0 +145,0 @@ } |
@@ -20,3 +20,3 @@ const cds = require('@sap/cds/lib') | ||
for (let p of LEGACY_OPTIONS) if ((x = compat[p])) linter_options[p] = x // eslint-disable-line no-cond-assign | ||
if (!Object.keys(linter_options).length) return [] | ||
const hasConfig = Object.keys(linter_options).length | ||
@@ -27,6 +27,6 @@ const reflectedCsn = cds.reflect(extCsn) | ||
const messages = [ | ||
...new NamespaceChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options), | ||
...new AnnotationsChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options), | ||
...new AllowlistChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options), | ||
...new CodeChecker().check(reflectedCsn, compileBaseDir, linter_options) | ||
...(hasConfig ? new NamespaceChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options) : []), | ||
...new AnnotationsChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options), // always mandatory | ||
...(hasConfig ? new AllowlistChecker().check(reflectedCsn, reflectedFullCsn, compileBaseDir, linter_options) : []), | ||
...new CodeChecker().check(reflectedCsn, compileBaseDir, linter_options) // always mandatory | ||
] | ||
@@ -33,0 +33,0 @@ deduplicateMessages(messages) |
@@ -82,5 +82,5 @@ const cds = require('@sap/cds/lib'), { fs, path, tar, rimraf } = cds.utils | ||
const async = cds.context.http?.req?.headers?.prefer === 'respond-async' | ||
await activate(ID, null, tenant, csvs, async) | ||
await activate(tenant, csvs, async) | ||
} | ||
module.exports = { push, pull } |
@@ -1,3 +0,1 @@ | ||
const { URL } = require('url'); | ||
const AuthProvider = require('./AuthProvider'); | ||
@@ -4,0 +2,0 @@ const { assertDefined } = require('./util/SecretsUtil'); |
@@ -26,3 +26,3 @@ const cds = require('@sap/cds/lib'); | ||
module.exports = async function token(request, response) { | ||
async function token(request, response) { | ||
if (request.method === 'HEAD') { | ||
@@ -33,12 +33,2 @@ return response.status(204).send(); | ||
const { credentials } = cds.env.requires.auth; | ||
const isEmpty = o => !o || Object.keys(o).length === 0; | ||
if (request.method === 'GET' && isEmpty(request.query)) { | ||
const passcodeUrl = new AuthProvider(credentials, {}).passcodeUrl; | ||
DEBUG?.(`Sending passcode URL to client:`, passcodeUrl); | ||
return response.status(200).send({ | ||
passcodeUrl | ||
}); | ||
} | ||
const query = request.method === 'POST' | ||
@@ -85,1 +75,17 @@ ? await parseBody(request) | ||
} | ||
async function authMeta(request, response) { | ||
const { credentials } = cds.env.requires.auth; | ||
const passcodeUrl = new AuthProvider(credentials, {}).passcodeUrl; | ||
DEBUG?.(`Sending passcode URL to client:`, passcodeUrl); | ||
// NOTE: Use snake_case for properties for compatibility with RFC 8414. | ||
return response.status(200).send({ | ||
passcode_url: passcodeUrl | ||
}); | ||
} | ||
module.exports = { | ||
token, | ||
authMeta | ||
} |
@@ -219,3 +219,3 @@ const crypto = require('crypto') | ||
try { | ||
const cqn = SELECT('csn').from('cds.xt.Extensions') | ||
const cqn = SELECT(['csn','tag']).from('cds.xt.Extensions').orderBy('tag','timestamp') | ||
if (activated) cqn.where('activated=', 'database') | ||
@@ -226,6 +226,9 @@ const exts = await cds.db.run(cqn) | ||
const merged = { extensions: [], definitions: {} } | ||
for (const { csn } of exts) { | ||
let lastTag | ||
for (const { csn, tag } of exts) { | ||
if (lastTag === tag) continue // skip duplicates that might have been created due to race conditions | ||
const {definitions,extensions} = JSON.parse(csn) | ||
if (definitions) Object.assign (merged.definitions, definitions) | ||
if (extensions) merged.extensions.push (...extensions) | ||
lastTag = tag | ||
} | ||
@@ -232,0 +235,0 @@ return merged |
@@ -16,4 +16,2 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires | ||
const useOldIm = cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager'] | ||
// FIXME: Do that check in a better way, as ExtensibilityService is always on with mtx-sidecar preset | ||
@@ -25,3 +23,3 @@ const isExtensible = cds.requires.extensibility || cds.requires['cds.xt.ExtensibilityService'] | ||
const hana = useOldIm ? require('./hana/inst-mgr') : require('./hana/srv-mgr') | ||
const hana = require('./hana/srv-mgr') | ||
exports.activated = 'HANA Database' | ||
@@ -88,3 +86,3 @@ | ||
// @sap/instance-manager compat | ||
// @sap/instance-manager API compat | ||
const compat = 'provisioning_parameters' in createParams || 'binding_parameters' in createParams | ||
@@ -190,7 +188,6 @@ if (compat) { | ||
const out = await fs.mkdirp(outRoot,'src','gen'), gen = [] | ||
cds.env.cdsc = main.env.cdsc // add cdsc options from main | ||
const options = { messages: [], sql_mapping: cds.env.sql.names, assertIntegrity:false } | ||
const { definitions: hanaArtifacts } = cds.compiler.to.hdi.migration(csn, options); | ||
const hanaArtifacts = _compileToHana(csn) | ||
const { getArtifactCdsPersistenceName } = cds.compiler | ||
const migrationTables = new Set(cds.reflect(csn) | ||
@@ -201,7 +198,2 @@ .all(item => item.kind === 'entity' && item['@cds.persistence.journal']) | ||
if (options.messages.length > 0) { | ||
// REVISIT: how to deal with compiler info and warning messages | ||
DEBUG?.('cds compilation messages:', options.messages) | ||
} | ||
for (const { name, suffix, sql } of hanaArtifacts) { | ||
@@ -236,3 +228,3 @@ if (suffix !== '.hdbtable' || !migrationTables.has(name)) { | ||
if (e.code !== 'EACCES') throw e | ||
LOG?.(`Using temporary directory ${TEMP_DIR} for build result`) | ||
LOG?.(`using temporary directory ${TEMP_DIR} for build result`) | ||
const out = path.join(TEMP_DIR, 'gen', `${tenant}${folderSuffix}`) | ||
@@ -285,8 +277,10 @@ await mkdirp(out) | ||
await fs.write ({ file_suffixes: { | ||
csv: { plugin_name: 'com.sap.hana.di.tabledata.source' }, | ||
hdbconstraint: { plugin_name: 'com.sap.hana.di.constraint' }, | ||
hdbindex: { plugin_name: 'com.sap.hana.di.index' }, | ||
hdbtable: { plugin_name: 'com.sap.hana.di.table' }, | ||
hdbtabledata: { plugin_name: 'com.sap.hana.di.tabledata' }, | ||
hdbview: { plugin_name: 'com.sap.hana.di.view' } | ||
csv: { plugin_name: 'com.sap.hana.di.tabledata.source' }, | ||
hdbconstraint: { plugin_name: 'com.sap.hana.di.constraint' }, | ||
hdbindex: { plugin_name: 'com.sap.hana.di.index' }, | ||
hdbtable: { plugin_name: 'com.sap.hana.di.table' }, | ||
hdbtabledata: { plugin_name: 'com.sap.hana.di.tabledata' }, | ||
hdbview: { plugin_name: 'com.sap.hana.di.view' }, | ||
hdbcalculationview: { plugin_name: 'com.sap.hana.di.calculationview' }, | ||
hdbeshconfig: { plugin_name: 'com.sap.hana.di.eshconfig' } | ||
}}) .to (out,'src','gen','.hdiconfig') | ||
@@ -314,3 +308,3 @@ } | ||
if (/authentication failed/i.test(e.message) || /SSL certificate validation failed/i.test(e.message)) { | ||
const hana = useOldIm ? require('./hana/inst-mgr') : require('./hana/srv-mgr') | ||
const hana = require('./hana/srv-mgr') | ||
return hana.get(tenant, { disableCache: true }) | ||
@@ -331,1 +325,26 @@ } else { | ||
function _compileToHana(csn) { | ||
cds.env.cdsc = main.env.cdsc // add cdsc options from main | ||
const options = { messages: [], sql_mapping: cds.env.sql.names, assertIntegrity: false } | ||
let definitions = [] | ||
if (cds.compile.to.hana) { | ||
const files = cds.compile.to.hana(csn, options); | ||
for (const [content, { file }] of files) { | ||
if (path.extname(file) !== '.json') { | ||
const { name, ext: suffix } = path.parse(file) | ||
definitions.push({ name, suffix, sql: content }) | ||
} | ||
} | ||
} else { | ||
// compatibility with cds 7 | ||
const r = cds.compiler.to.hdi.migration(csn, options) | ||
definitions = r.definitions | ||
} | ||
if (options.messages.length > 0) { | ||
// REVISIT: how to deal with compiler info and warning messages | ||
DEBUG?.('cds compilation messages:', options.messages) | ||
} | ||
return definitions | ||
} |
@@ -15,3 +15,3 @@ const { join } = require('path') | ||
if (e.code !== 'EACCES') throw e | ||
LOG?.(`Using temporary directory ${TEMP_DIR} for deployment logs`) | ||
LOG?.(`using temporary directory ${TEMP_DIR} for deployment logs`) | ||
return join(TEMP_DIR, 'logs') | ||
@@ -72,2 +72,7 @@ } | ||
const hdi_opts = _parse_env ('HDI_DEPLOY_OPTIONS', options) | ||
try { require.resolve('hdb') } catch (e) { | ||
if (e.code === 'MODULE_NOT_FOUND') hdi_opts.use_hdb = false | ||
else throw e | ||
} | ||
if (hdi_opts.use_hdb !== false) hdi_opts.use_hdb = true | ||
env.HDI_DEPLOY_OPTIONS = JSON.stringify (hdi_opts) | ||
@@ -74,0 +79,0 @@ return env |
@@ -5,3 +5,3 @@ const https = require('https') | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx|sm') | ||
const { uuid } = cds.utils | ||
const { uuid, fs, path } = cds.utils | ||
const { cacheBindings = true } = cds.env.requires.multitenancy ?? {} | ||
@@ -13,2 +13,5 @@ const { sm_url, url, clientid, clientsecret, certurl, certificate, key } = cds.env.requires.db.credentials | ||
// In-memory storage -> later also distribute w/ Redis | ||
const instanceLocations = new Map, bindingLocations = new Map | ||
/* API */ | ||
@@ -20,36 +23,70 @@ | ||
const { binding_parameters, provisioning_parameters } = parameters ?? {} | ||
let _instance, service_instance_id | ||
let service_instance_id | ||
if (instanceLocations.has(tenant)) { | ||
const storedLocation = instanceLocations.get(tenant) | ||
LOG.info('polling ongoing instance creation for', { tenant }) | ||
const polledInstance = await _poll(storedLocation) | ||
service_instance_id = polledInstance.resource_id | ||
instanceLocations.delete(tenant) | ||
} else { | ||
try { | ||
_instance = await fetchApi('service_instances?async=true', { | ||
const _instance = await fetchApi('service_instances?async=true', { | ||
method: 'POST', | ||
data: { | ||
name, service_plan_id, parameters: provisioning_parameters, | ||
labels: { tenant_id: [tenant] }, | ||
} | ||
}) | ||
instanceLocations.set(tenant, _instance.headers.location) | ||
service_instance_id = (await _poll(_instance.headers.location)).resource_id | ||
instanceLocations.delete(tenant) | ||
} catch (e) { | ||
instanceLocations.delete(tenant) | ||
const status = e.status ?? 500 | ||
if (status === 409 || e.error === 'Conflict') { | ||
const instance = await _instance4(tenant) | ||
if (!instance.ready || !instance.usable) { | ||
const { type, state, errors } = instance?.last_operation ?? {} | ||
LOG.info(`detected faulty instance for tenant '${tenant}' in state '${state}' for operation type '${type}'`) | ||
if (type === 'create' && state === 'failed') { | ||
LOG.info(`removing and recreating faulty instance for tenant '${tenant}'`, | ||
DEBUG ? `with error: ${e.error}: ${e.description}. Last operation: ${errors?.error} ${errors?.description}` : '' | ||
) | ||
await remove(tenant) | ||
return create(tenant, parameters) | ||
} else { | ||
e.message ??= '' | ||
e.message += `${e.error}: ${e.description}. Last operation: ${errors?.error} ${errors?.description}` | ||
throw e | ||
} | ||
} | ||
service_instance_id = instance.id | ||
} else { | ||
cds.error(_errorMessage(e, 'creating', tenant), { status }) | ||
} | ||
} | ||
} | ||
if (bindingLocations.has(tenant)) { | ||
const storedLocation = bindingLocations.get(tenant) | ||
LOG.info(`ongoing binding creation for tenant ${tenant}, polling existing request`) | ||
try { | ||
await _poll(storedLocation) | ||
} finally { | ||
bindingLocations.delete(tenant) | ||
} | ||
} else { | ||
const _binding = await fetchApi('service_bindings?async=true', { | ||
method: 'POST', | ||
data: { | ||
name, service_plan_id, parameters: provisioning_parameters, | ||
labels: { tenant_id: [tenant] }, | ||
name: tenant + `-${uuid()}`, service_instance_id, binding_parameters, | ||
labels: { tenant_id: [tenant], service_plan_id: [service_plan_id], managing_client_lib: ['instance-manager-client-lib'] } | ||
} | ||
}) | ||
service_instance_id = (await _poll(_instance.headers.location)).resource_id | ||
} catch (e) { | ||
const status = e.status ?? 500 | ||
if (status === 409 || e.error === 'Conflict') { | ||
const instance = await _instance4(tenant) | ||
if (!instance.ready || !instance.usable) { | ||
const faultyInstance = await fetchApi(`service_instances/${instance.id}`) | ||
const errors = faultyInstance?.data?.last_operation?.errors | ||
e.message ??= '' | ||
e.message += `${e.error}: ${e.description}. Last operation: ${errors?.error} ${errors?.description}` | ||
throw e | ||
} | ||
service_instance_id = instance.id | ||
} else { | ||
cds.error(_errorMessage(e, 'creating', tenant), { status }) | ||
} | ||
bindingLocations.set(tenant, _binding.headers.location) | ||
await _poll(_binding.headers.location) | ||
bindingLocations.delete(tenant) | ||
} | ||
const _binding = await fetchApi('service_bindings?async=true', { | ||
method: 'POST', | ||
data: { | ||
name: tenant + `-${uuid()}`, service_instance_id, binding_parameters, | ||
labels: { tenant_id: [tenant], service_plan_id: [service_plan_id], managing_client_lib: ['instance-manager-client-lib'] } | ||
} | ||
}) | ||
await _poll(_binding.headers.location) | ||
const binding = { ...await get(tenant), tags: ['hana'] } | ||
@@ -108,3 +145,3 @@ return cacheBindings ? _bindings4.cached[tenant] = binding : binding | ||
const fieldQuery = `name eq '${await _instanceName4(tenant)}'` | ||
const instances = await fetchApi('service_instances?async=true', { | ||
const instances = await fetchApi('service_instances?async=true&attach_last_operations=true', { | ||
params: { fieldQuery } | ||
@@ -186,3 +223,3 @@ }) | ||
if (state === 'failed') return reject(errors[0] ?? errors) | ||
if (attempts > maxAttempts) return reject(new Error(`Polling ${location} timed out after ${maxTime} seconds`)) | ||
if (attempts > maxAttempts) return reject(new Error(`Polling ${location} timed out after ${maxTime} seconds with state ${state}`)) | ||
setTimeout(++attempts && _next, 3000, resolve, reject) | ||
@@ -199,2 +236,4 @@ } | ||
const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf8')) | ||
const fetchApi = async (url, conf = {}) => { | ||
@@ -204,4 +243,6 @@ conf.headers ??= {} | ||
conf.headers['Content-Type'] ??= 'application/json' | ||
conf.headers['Client-ID'] ??= 'cap-mtx-sidecar' | ||
conf.headers['Client-Version'] ??= version | ||
conf.baseURL ??= sm_url + '/v1/' | ||
return fetchResiliently(url, conf) | ||
return fetchResiliently(conf.baseURL + url, conf) | ||
} | ||
@@ -212,5 +253,5 @@ | ||
conf.method ??= 'GET' | ||
url = conf.baseURL ? conf.baseURL + url : url | ||
try { | ||
DEBUG?.('>', conf.method.toUpperCase(), url, inspect({ | ||
...(conf.headers && { headers: conf.headers }), | ||
...(conf.params && { params: conf.params }), | ||
@@ -241,6 +282,5 @@ ...(conf.data && { data: conf.data }) | ||
else return pruneAxiosErrors(error) | ||
} else if (status in { 408: 1, 502: 1, 504: 1 }) { | ||
delay = 300 * 2 ** (attempt - 1) | ||
} else { | ||
delay = 1000 * 3 ** (attempt - 1) | ||
} else { // S-curve instead of exponential backoff to allow for high number of reattempts (∞) | ||
const maxDelay = 30000, midpoint = 6, steepness = 0.4 | ||
delay = maxDelay * (1 + Math.tanh(steepness * (attempt - midpoint))) / 2 | ||
} | ||
@@ -247,0 +287,0 @@ await new Promise((resolve) => setTimeout(resolve, delay)) |
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
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
251768
63
4667
+ Added@sap/hdi@4.6.0(transitive)
+ Added@sap/hdi-deploy@5.3.2(transitive)
+ Added@sap/xsenv@5.4.0(transitive)
+ Addedasync@3.2.6(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addeddotenv@16.4.5(transitive)
+ Addedhandlebars@4.7.8(transitive)
+ Addedmicromatch@4.0.8(transitive)
+ Addedms@2.1.3(transitive)
+ Addedverror@1.10.1(transitive)
- Removed@sap/hana-client@2.20.22(transitive)
- Removed@sap/hdi@4.5.2(transitive)
- Removed@sap/hdi-deploy@4.9.5(transitive)
- Removed@sap/xsenv@4.2.0(transitive)
- Removedasync@3.2.3(transitive)
- Removeddebug@3.1.04.3.3(transitive)
- Removeddotenv@10.0.0(transitive)
- Removedhandlebars@4.7.7(transitive)
- Removedhdb@0.19.8(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedmicromatch@4.0.7(transitive)
- Removedms@2.0.02.1.2(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedverror@1.10.0(transitive)
Updated@sap/hdi-deploy@>=4