@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
176 lines (156 loc) • 5.92 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.$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
}