UNPKG

@sap/cds-dk

Version:

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

246 lines (218 loc) 8.72 kB
#!/usr/bin/env node const cds = require ('../lib/cds.js') const { regex4 } = require ('./util/cli.js') const { Minify } = require ('./minify.js') class Export extends Minify { options = Export.options // for IntelliSense static options = { ...super.options, services: /all/, from: ['*'], to: './apis/*', cleanse: { _excluding: 'keep', '@restrict': true, '@requires': true, '@cds.api.': true, '@cds.on.': true, '@UI.': true, includes: true, ignored: true, assocs: true, texts: true, views: true, }, reuse: [ '@sap/cds/common' ], plugin: false, texts: false, data: false, as: 'csn', for: 'npm', force: false, } static shortcuts = { ...super.shortcuts, to:'2', for:'4' } static filename = __filename parseArgs (conf) { if (!process.stdout.isTTY) this.options.to = '-' let [sources,o] = super.parseArgs (conf) // ----------------------------------------------------------------------- // Java-specific adjustments -> shall this stay for GA? if (o.for === 'mvn' && !o.to.startsWith('./') && !o.to.startsWith('/')) o.to = './src/main/resources/cds/' + o.to // ----------------------------------------------------------------------- if (o.from && o.from != '*') { // variant: cds-export <services> --from <models> if (o.services == '/all/') o.services = regex4 (sources) sources = o.from } return [ this.sources = o.from = sources, this.options = o ] } async requires (models) { if (models == 'none' || models == '') return {check(){}} const { definitions: defs, $sources } = await cds.load (models, 'parsed') const $map = Object.fromEntries ($sources.map ((s,i) => [ local(s), models[i] ])) return Object.assign (new Set, { check(n) { return n in defs && this.add ($map[defs[n].$location.file]) }}) } async cleanse (csn, o = this.options) { const reused = await this.requires (o.reuse) const { definitions: defs } = csn = { ...csn, definitions: { ...csn.definitions }} const { cleanse } = o // cleanse model... for (const [each,d] of Object.entries (defs)) { if (reused.check(each)) delete defs [each] else if (cleanse.ignored && d['@cds.api.ignore']) delete defs [each] else if (cleanse.texts && each.endsWith('.texts')) delete defs [each] else if (d.kind === 'entity') _cleanse (d, cleanse) } if (reused.size > 0) csn.requires = [...reused] // cleanse entities... function _cleanse (d, cleanse) { if (cleanse.managed) { delete d.elements.createdAt delete d.elements.createdBy delete d.elements.modifiedAt delete d.elements.modifiedBy } if (cleanse.users) { delete d.elements.createdBy delete d.elements.modifiedBy } if (cleanse.views) { delete d.projection delete d.query } if (cleanse.texts) { delete d.elements.localized delete d.elements.texts } // cleanse elements... for (const [each,e] of Object.entries(d.elements)) { if (cleanse.ignored && e['@cds.api.ignore']) return delete d.elements[each] if (cleanse.texts) delete e.localized if (cleanse.assocs) delete e.keys // cleanse .keys added by cdsc } } return super.cleanse (csn,o) } async output (minified,o) { if (this.options.to === '-') return super.output (minified) if (this.options.to.endsWith('/*')) this.options.to = this.export_to() console.log(`\n${this.name}ing APIs to`, cds.utils.local(this.options.to), '...\n') const exports = []; exports.push(...this.export_models (minified)) if (o.texts) exports.push (...this.export_texts (minified)) if (o.data) exports.push (this.export_data (minified)) if (o.for === 'npm') exports.push (this.export_package_json()) await Promise.all (exports) console.log() } export_to() { const to = this.options.to.replace('/*','') const { resolve, basename } = cds.utils.path const { sources } = this if (sources.length === 1 && sources[0].endsWith('.cds')) return resolve (to, this.package_name = basename(sources[0]).slice(0,-4)) const defs = this.min.definitions const services = Object.keys(defs).filter (k => defs[k].kind === 'service') if (services.length === 1) return resolve (to, this.package_name = services[0]) else throw `\n Can't determine an output folder. \n Please specify one using --to option. \n` } async export_package_json (pkg, o = this.options) { if (!o.force && exists (path.join (this.options.to, 'package.json'))) return null if (!pkg) { let { name, version } = await read ('package.json') name += '-' + cds.utils.path.basename (this.options.to) pkg = { name, version } if (o.plugin) { let requires = Object.fromEntries (Object.entries(this.min.definitions) .filter (([,d]) => d.kind === 'service') .map (([name]) => [ name, true ])) pkg.cds = {requires} } } if (o.plugin) await this.write ('// just a tag file for plug & play') .to ('cds-plugin.js', false) return this.write (pkg) .to ('package.json') } *export_models (model, o = this.options) { for (let d of Object.values(model.definitions)) if (d.kind === 'service') d['@cds.external'] ??= 2 yield this.write ( `// This file acts as a central facade to exported service definitions.\n`+ `// You can modify it to tweak things, without your changes being overridden.\n`+ `using from './services';` ) .to ('index.cds', false) if (o.as === 'cdl') { const cdl = cds.compile.to.cdl (model) yield this.write (cdl) .to ('services.cds') } else { const csn = cds.compile.to.json (model) yield this.write (csn) .to ('services.csn') } } *export_texts() { const common = cds.load.properties (require.resolve('@sap/cds/_i18n/i18n.properties')) const bundles = cds.i18n.labels.all() const defaults = cds.i18n.labels.defaults const serialize = texts => Object.entries(texts).map(([k,v]) => k +'='+ v.replace(/'/g, "''")).join('\n') const write = texts => ({ to: file => this.write(serialize(texts)).to(file) }) // minify fallback bundle entries const fallback = bundles[''] = {} for (let key in defaults) { if (key in common) continue fallback[key] = defaults[key] } yield write(fallback) .to ('_i18n/i18n.properties') // minify other bundles for (let [locale,texts] of Object.entries(bundles)) { const min = bundles[locale] = {} for (let key in texts) { if (key in common) continue if (texts[key] === defaults[key]) continue min[key] = texts[key] } if (Object.keys(min).length > 0) write(min) .to (`_i18n/i18n_${locale}.properties`) } } async export_data (model) { const db = cds.db = await cds.deploy ('*',{silent:true}) const out = path.resolve (this.options.to, 'data') await fs.promises.rm (out, { recursive: true, force: true }) await mkdirp (this.options.to, 'data') for (let each of cds.linked(model).entities) { await _export (db.model.definitions[each.name]) } async function _export (e) { if (!e || e['@cds.persistence.skip']) return const file = path.join (out, `${e.name}.csv`) const columns = Object.keys(e.elements) //.filter(k => !e.elements[k]['@Core.Computed']) const rows = await SELECT.from(e).columns(columns) let csv = fs.createWriteStream (file), i=0 for (let r of await rows) { if (i++ === 0) csv.write (Object.keys(r).join(',') +'\n') csv.write (Object.values(r).map(quoted).join(',') +'\n') } csv.end() console.log (DIMMED,' >', local(file), RESET) } function quoted (x) { if (typeof x === 'string') { if (x.startsWith('"') && x.endsWith('"')) return x // already quoted if (x.endsWith(',')) x = x.slice(0, -1) // remove trailing comma if (x.includes(',') || x.includes('\n')) return `"${x.replace(/"/g, '""')}"` } return x } } write (x) { return { to: async (filename, force=true) => { if (!x) return; else if (x.then) x = await x const file = path.join (this.options.to, filename) if (!force && !this.options.force && exists(file)) return console.log (DIMMED,' >', local(file), RESET) return write(x).to (file) }}} } const { read, write, exists, mkdirp, local, path, fs } = cds.utils const { DIMMED, RESET } = cds.utils.colors module.exports = Object.assign (Export._for_cds_dk(), { Export })