UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

181 lines (161 loc) 6.32 kB
const cds = require('../../../cds') const { fs: { promises: { rename } }, read, write, path: { basename, dirname, join }, exists } = cds.utils const { readProject } = require('../../projectReader') const { renderAndCopy } = require('../../template') const mvn = require('../../mvn') const { dim } = require('../../../util/term') const { filterStringAsRegex } = require('../../add') module.exports = class Handler extends require('../../plugin') { static help() { return 'handler stubs for service entities, actions and functions' } options() { return { 'filter': { type: 'string', short: 'f', help: `Filter for entities, actions or functions matching the given pattern. For Node.js, if it contains meta characters like '^' or '*', it is treated as a regular expression, otherwise as an include pattern, i.e /.*bookshop.*/i For Java, only '*' and '**' as suffix wildcards are allowed, as in 'my.bookshop.*' or 'my.**'` }, 'out': { type: 'string', short: 'o', help: `Custom output directory. For Java, the default is 'handlers'. For Node.js, the default is 'srv'.` } } } canRun() { return true } async run() { const proj = readProject() if (proj.isJava) { await mvn.generate('handler') } else { await createNodeHandler(proj) } } } /** * @param {ReturnType<readProject} proj */ async function createNodeHandler(proj) { const { filter, force, out=cds.env.folders.srv } = cds.cli.options const csn = await loadModel() if (!csn) return console.log(dim(`> skipping, no model found`)) const nameFilter = filterStringAsRegex(filter) for (const service of csn.services) { if (service['@cds.external']) continue if (!service.$location.file) continue proj.serviceName = service.name proj.serviceClass = service.name.split('.').pop() proj.servicePath = proj.serviceName ? (proj.serviceName.replace(/\./g, '/')) : '' proj.entities = Object.entries(service.entities) .filter(([name]) => name.match(nameFilter)) .filter(([name]) => !name.match(/[._]texts$/)) .filter(([, e]) => !e['@cds.autoexposed']) // only consider non-autoexposed entities to reduce clutter .map(([name]) => ({ name, dataVar: name.charAt(0).toLowerCase() + name.slice(1) // lower case first letter })) if (proj.entities.length) proj.entityNames = proj.entities.map(e => e.name).join(', ') proj.operations = Object.keys(service.actions) .filter(name => name.match(nameFilter)) .map(name => ({ name, dataVar: name.charAt(0).toLowerCase() + name.slice(1) // lower case first letter })) const all = [...proj.entities, ...proj.operations] if (all.length > 4) proj.allNames = `\n ${all.map(e => e.name).join(',\n ')}\n` else if (all.length) proj.allNames = all.map(e => e.name).join(', ') if (proj.extendsApp) { await createExtCodeHandler(service, proj, out, force) } else { const destFileType = proj.isTypescript ? '.ts' : '.js' const outPath = join(out, basename(service.$location.file.replace('.cds', destFileType))) const destFile = join(cds.root, outPath) if (!force && exists(destFile)) { console.log(dim(`> skipping ${outPath}`)) continue } console.log(dim(`> writing ${outPath}`)) const destPath = dirname(destFile) await renderAndCopy(join(__dirname, 'files/standard'), destPath, proj) await rename(join(destPath, 'handler.xs'), destFile) } } } async function createExtCodeHandler(service, proj, out, force) { const destFileType = '.js' // only JS for now, a TS toolchain in an ext. project would be too complex const srvOutPath = join(cds.root, out, service.name) // entities const events = [['after', 'READ'], ['before', 'CREATE'], ['before', 'UPDATE']] for (const ent of proj.entities) { proj.entityName = ent.name const entityOutPath = join(srvOutPath, ent.name) for (const [when, event] of events) { proj.when = when proj.event = event const outPath = join(entityOutPath, `${when}-${event}${destFileType}`) if (!force && exists(outPath)) { console.log(dim(`> skipping ${outPath}`)) continue } console.log(dim(`> writing ${outPath}`)) await renderAndCopy(join(__dirname, 'files/extension'), entityOutPath, proj) await rename(join(entityOutPath, 'handler.xs'), outPath) } } // unbound actions proj.when = 'on' proj.entityName = '' for (const op of proj.operations) { proj.event = op.name const outPath = join(srvOutPath, `on-${op.name}${destFileType}`) if (!force && exists(outPath)) { console.log(dim(`> skipping ${outPath}`)) continue } console.log(dim(`> writing ${outPath}`)) await renderAndCopy(join(__dirname, 'files/extension'), dirname(outPath), proj) await rename(join(dirname(outPath), 'handler.xs'), outPath) } // package.json const pkgJson = await read('package.json') const pkgJsonDelta = require('./files/extension/package.json') cds.utils.merge ? cds.utils.merge(pkgJson, pkgJsonDelta) : merge(pkgJson, pkgJsonDelta) await write('package.json', pkgJson, { spaces: 2 }) } /** * @returns { Promise<import('@cap-js/cds-types').linked.LinkedCSN | null> } */ async function loadModel() { try { return cds.linked(cds.minify(await cds.load(cds.env.roots))) } catch (err) { if (err.code === 'MODEL_NOT_FOUND') return null throw new Error(`Error compiling CDS files. Run 'npm install' and try again.`, {cause:err}) } } // cds10: remove, copied from cds-utils.js /** * Polyfill for cds.utils.merge * Simple helper to deep-merge two or more objects. * Entries from `xs` overwrite entries in `o`. * @example cds.utils.merge({foo:1},{bar:2},{baz:3}) * @returns `o` with entries from `xs` merged in. */ function merge (o,...xs) { let v; for (let x of xs) for (let k in x) if (k === '__proto__' || k === 'constructor') continue //> avoid prototype pollution else o[k] = is_object(v=x[k]) ? merge(o[k]??={},v) : v return o } const is_object = x => typeof x === 'object' && x !== null && !is_array(x) const is_array = Array.isArray