@sap/cds-mtxs
Version:
SAP Cloud Application Programming Model - Multitenancy library
127 lines (105 loc) • 4.89 kB
JavaScript
const cds = require('@sap/cds/lib')
const { t0_ } = require('../../lib/utils')
const Tenants = 'cds.xt.Tenants'
const LOG = cds.log('mtx')
const main = require('../config')
exports.activated = 'Generic Metadata'
const t0 = cds.env.requires.multitenancy?.t0 ?? 't0'
// Add database-agnostic metadata handlers to DeploymentService...
cds.on('serving:cds.xt.DeploymentService', ds => {
const lazyT0 = cds.env.requires['cds.xt.DeploymentService']?.lazyT0 ?? cds.env.requires.multitenancy?.lazyT0
ds.before('*', req => {
const { tenant } = req?.data ?? {}
if (tenant) cds.context = { tenant }
})
ds.before('subscribe', req => {
if (lazyT0 && req.data.tenant !== t0) {
return _resubscribeT0IfNeeded(req.data.options?._, true)
}
})
ds.before('upgrade', async req => {
if (main.requires.extensibility) return // no checks needed
if (cds.env.requires['cds.xt.DeploymentService']?.upgrade?.skipExtensionCheck === true) return
// duplicate code, but it must be ensured that the tenant is set for the following operations
const { tenant } = req?.data ?? {}
if (tenant) cds.context = { tenant }
let existingExt
try {
existingExt = await SELECT.one(1).from('cds.xt.Extensions')
} catch (e) {
LOG.debug('No extensions found', e) // ok, no problem
}
if (existingExt) cds.error(`Extensions exist, but extensibility is disabled. Upgrade aborted to avoid data loss`, { status: 500 })
})
ds.after('subscribe', async (_, req) => {
const { tenant, metadata, schema } = req.data
if (tenant === t0) return
try {
// can't use UPSERT here so @cds.on.insert still works for createdAt
await t0_(INSERT.into(Tenants, { ID: tenant, metadata: JSON.stringify(metadata), ...(schema ? { schema } : {}) }))
} catch (e) {
if (e.message === 'ENTITY_ALREADY_EXISTS' || e.code === 301 || e.message.match(/unique constraint/i)) {
await t0_(async () => {
const row = await SELECT.one.from(Tenants).columns('metadata').where({ ID: tenant })
let existingMetadata = {}
try {
existingMetadata = JSON.parse(row.metadata || '{}');
} catch (error) {
LOG.error('Failed to parse metadata to JSON', { error, metadata });
}
const merged = JSON.stringify(cds.utils.merge(existingMetadata, metadata))
await UPSERT.into(Tenants, { ID: tenant, metadata: merged, ...(schema ? { schema } : {}) })
})
} else throw e
}
LOG.info('subscribed tenant', tenant)
})
// ds.after(['upgrade', 'extend'], async (_, req) => {
// const { tenant } = req.data
// if (tenant === t0) return
// const version = JSON.stringify({
// version: require(cds.root + '/package.json').version
// })
// await t0_(UPSERT.into(Tenants, { ID: tenant, version }))
// LOG.info('upgraded tenant', tenant)
// })
ds.after('unsubscribe', async (_, req) => {
const { tenant } = req.data
if (tenant !== t0) await t0_(DELETE.from(Tenants).where({ ID: tenant }))
LOG.info('unsubscribed tenant', tenant)
})
ds.prepend(() => {
ds.on('getTenants', async () => {
return (await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.from(Tenants, tenant => { tenant.ID }))
)).map(({ ID }) => ID)
})
})
const { getArtifactCdsPersistenceName } = require('@sap/cds-compiler')
function _getT0TenantsTableName(csn) {
return cds.requires.db.kind === 'hana' ?
getArtifactCdsPersistenceName('cds.xt.Tenants', cds.env.sql.names || 'plain', csn, 'hana')
: 'cds_xt_Tenants'
}
// Needs to be exposed for lazyT0 (CALM use case)
const _resubscribeT0IfNeeded = module.exports.resubscribeT0IfNeeded = async function (params, onlyDeploy) {
if (lazyT0 && cds.requires.db.kind === 'hana' && !onlyDeploy) {
const hana = require('./hana/hdi-ctnr-mgr').defaultInstance() // REVISIT: workaround for lazyT0
try { await hana.get(t0, { disableCache: true }) } catch (e) {
if (e.status === 404) return
else throw e
}
}
await cds.connect() // REVISIT: Ideally shouldn't be necessary
const effectiveParams = JSON.parse(JSON.stringify(params ?? {})) // clone to preserve params for original tenant subscription
if (effectiveParams?.hdi?.create) delete effectiveParams.hdi.create.hana_tenant_id // never use hana_tenant_id for t0
await ds.tx({ tenant: t0 }, async tx => {
const csn = await cds.load(`${__dirname}/../../db/t0.cds`)
const columns = await tx.getColumns(t0, _getT0TenantsTableName(csn), effectiveParams)
const needsT0Redeployment = !columns.some(c => c.toLowerCase() === 'version')
if (!needsT0Redeployment) return
await tx.subscribe({ tenant: t0, options: { csn, _: effectiveParams } })
})
}
cds.once('served', () => _resubscribeT0IfNeeded())
})