UNPKG

@sap/cds-mtxs

Version:

SAP Cloud Application Programming Model - Multitenancy library

122 lines (112 loc) 4.32 kB
const cds = require('@sap/cds') const LOG = cds.log('mtx'), { read, readdir, path, tar } = cds.utils const { promisify } = require('util') const readData = async function (extension, root) { 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 } const csvs = [] try { const dirents = await readdir(path.join(root, 'data')) for (const dirent of dirents) { const basename = path.basename(dirent) csvs[basename] = await read(path.join(root, 'data', dirent)) } } catch(e) { if (e.code !== 'ENOENT') throw e } return { extCsn, bundles, csvs } } // REVISIT: opt-in/out retry mechanism on runtime layer? // Sketch: cds.tx({ tenant: 't1', retries: 2 }, ...) // Not for all error cases a retry is an appropriate handling mechanism (e.g. 403) // -> error code allow/blocklist /** * @template T * @param {() => Promise<T>} fn * @param {number} [retryCount=5] * @param {number} [initialRetryGap=5000] * @returns {Promise<T>} */ const retry = async (fn, retryCount = cds.requires.multitenancy?.retries ?? 10, initialRetryGap = 5000) => { let errorCount = 0 let finalError let retryGap = initialRetryGap while (errorCount < retryCount - 1) { try { return await fn() } catch (e) { if (e instanceof TypeError) throw e // db unique constraint failure, previously masked as code 400 if (e.code === 301 || e.message.match(/unique constraint/i)) throw e if (e.code === 400) throw e if (e.code === 404) throw e // REVISIT: ugly -> shouldn't have to code for specific DBs if (e.code === 'SQLITE_ERROR') throw e if (e.code === 259 && e.sqlState === 'HY000' && /could not find table/i.test(e.message)) throw e finalError = e errorCount++ LOG.error('attempt', errorCount, 'errored with', e, '- retrying attempt', errorCount + 1, 'of', retryCount) await promisify(setTimeout)(retryGap) retryGap *= 1.5 } } LOG.error(`exceeded maximum number of ${retryCount} retries`) throw finalError } const t0 = cds.env.requires.multitenancy?.t0 ?? 't0' let _t0Csn const t0_ = async (query) => retry(async () => { const tX = cds.env.features?.t0wO ? 't0wO' : 't0' _t0Csn ??= cds.compile.for.nodejs( await cds.load(`${__dirname}/../db/${tX}.cds`, { silent: true }) ) return cds.tx({ tenant: t0 }, tx => { tx.model = _t0Csn; return tx.run(query) }) }) async function token(credentials, { query = {}, form = {}, subdomain } = {}) { const { clientid, clientsecret, certurl, url, certificate, key } = credentials ?? {} const u = new URL(certurl || url) if (subdomain) u.hostname = u.hostname.replace(/^[^.]+/, subdomain) u.pathname = '/oauth/token' Object.entries(query).forEach(([k, v]) => u.searchParams.append(k, v)) const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: clientid, ...form }) const headers = { 'Content-Type': 'application/x-www-form-urlencoded' } let raw if (certificate) { raw = await new Promise((resolve, reject) => { const req = require('node:https').request({ hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search, cert: certificate, key, method: 'POST', headers }, res => { const chunks = [] res.on('data', x => chunks.push(x)) res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) resolve(Buffer.concat(chunks).toString()) else reject(new Error(Buffer.concat(chunks).toString(), { status: res.statusCode })) }) }) req.on('error', reject) req.end(body.toString()) }) } else { headers.Authorization = 'Basic ' + Buffer.from(`${clientid}:${clientsecret}`).toString('base64') headers.Accept = 'application/json' const res = await fetch(u.toString(), { method: 'POST', headers, body: body.toString() }) if (!res.ok) throw cds.error(await res.text(), { status: res.status }) raw = await res.text() } return raw } module.exports = { t0_, retry, readData, token }