@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
181 lines (161 loc) • 6.32 kB
JavaScript
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