UNPKG

@sap/cds-dk

Version:

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

176 lines (156 loc) 5.92 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.$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') deepMerge(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}) } } function deepMerge(obj1, obj2) { for (const key in obj2) { if (Object.prototype.hasOwnProperty.call(obj2, key)) { if (obj2[key] instanceof Object && obj1[key] instanceof Object) { obj1[key] = deepMerge(obj1[key], obj2[key]) } else { obj1[key] = obj2[key] } } } return obj1 }