UNPKG

@sap/cds-dk

Version:

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

187 lines (163 loc) 6.55 kB
const cds = require('../../../cds') const { exists, fs } = cds.utils const { dim } = require('../../../util/term') const { resolve, join, relative } = require('path') const { URLSearchParams } = require('url') const { env4 } = require('../../projectReader') const asJson = require('./as-json') const asCsv = require('./as-csv') const { filterStringAsRegex } = require('../../add') module.exports = class DataTemplate extends require('../../plugin') { static help() { return 'add CSV headers for modeled entities' } options() { return { 'filter': { type: 'string', short: 'f', help: `Filter for entities matching the given pattern. If it contains meta characters like '^' or '*', it is treated as a regular expression, otherwise as an include pattern, i.e /.*pattern.*/i` }, 'data:for': { type: 'string', help: `Deprecated. Use '--filter' instead.` }, 'records': { type: 'number', short: 'n', help: 'The number of records to be created for each entity.' }, 'content-type': { type: 'string', short: 'c', help: 'The content type of the data. One of "json" or "csv".' }, 'out': { type: 'string', short: 'o', help: 'The output target folder.' } } } async run() { if (cds.cli.options.for) { throw `\nError: Did you mean \`... --data:for\` ?` } let { force, out, 'content-type' : contentType='csv', records=1 } = cds.cli.options if (typeof records === 'string') records = parseInt(records) if (Number.isNaN(records) || records < 1) records = 1 const [filterName, filterQueryStr] = (cds.cli.options['filter'] ?? cds.cli.options['data:for'] ?? '').split(':') const nameFilter = filterStringAsRegex(filterName) const dataForParams = parseQueryStr(filterQueryStr) const { queries } = dataForParams const env = env4('production') let dest = (typeof out === 'string') // target folder ? out : getDefaultTargetFolder(env) dest = resolve(cds.root, dest) let csn = await cds.load([cds.env.roots, join(`${cds.env.folders.srv}`, `external`) ]) // normal CSN. Default paths and models imported with `cds import` // Skip of unused reuse entities like Language, Country, Currency csn = cds.minify(csn) csn = cds.reflect(csn) // reflected model (adds additional helper functions) const data = csn.all('entity') .filter (e => e.name.match(nameFilter)) // --for prefix|regex .filter (e => queries || !e.query) // remove all projection-like entities unless specified differently .reduce((all, e) => { all[e.name] = []; return all }, {}) await asJson(data, csn, records, {contentType}) if (contentType === 'csv') { const headerOnly = !cds.cli.options['records'] // compat to old behavior which only creates headers await asCsv(data, csn, Object.assign({ headerOnly }, dataForParams)) } // write files for (const name of Object.keys(data).sort()) { writeFileFor(csn.definitions[name], data[name], csn, dest, force) } } } module.exports.asJson = asJson module.exports.asCsv = asCsv function parseQueryStr(queryStr) { const params = new URLSearchParams(queryStr) // e.g. key1&key2=false const res = Object.fromEntries(params) for (let [k, v] of Object.entries(res)) { if (v.length === 0 || v === 'true') res[k] = true else if (v === 'false') res[k] = false } return res } function writeFileFor (entity, data, csn, dest, force) { let dataFileName const namespace = getNamespace(csn, entity.name) if (!namespace || namespace == entity.name) { dataFileName = `${entity.name}` } else { const entityName = entity.name.replace(namespace + '.', '') dataFileName = `${namespace}-${entityName}` } dataFileName += Array.isArray(data) ? '.json' : '.csv' let dataFilePath = join(dest, dataFileName) let fileExists = exists(join(dest, dataFileName)) if (entity.name.endsWith('.texts')) { // handle '.texts' entities (for localized elements) differently: // if there is already file exist with '_texts' (old cds versions) - overwrite this one (when --force is used) // otherwise use the new '.texts' format const dataFileNameOldFormat = dataFileName.replace('.texts.csv','_texts.csv') const dataFilePathOldFormat = join(dest, dataFileNameOldFormat) if (exists(dataFilePathOldFormat)) { dataFileName = dataFileNameOldFormat dataFilePath = join(dest, dataFileName) fileExists = true } } let relFilePath = dataFilePath if (dataFilePath.indexOf(cds.root) === 0) { // use relative path in log (for readability), only when data files are added within the project // (potentially can be located anywhere using the --out parameter) relFilePath = relative(cds.root, dataFilePath) } // continue only if file not already exists, or '--force' option provided if (fileExists && !force) { console.log(` ${dim('skipping ' + relFilePath)}`) return } if (typeof data === 'object') data = JSON.stringify(data, null, 2) if (data.length) { if (!exists(dest)) fs.mkdirSync(dest, {recursive: true}) fs.writeFileSync(dataFilePath, data) if (fileExists) console.log(` ${dim('overwriting ' + relFilePath)}`) else console.log(` ${dim('creating ' + relFilePath)}`) } } function getDefaultTargetFolder (env) { const { db } = env.folders // csv files should be located in the 'db/data' folder unless a 'db/csv' folder already exists return join(db, exists(join(db, 'csv')) ? 'csv' : 'data') } // Logic is taken from cds-compile function getNamespace(csn, artifactName) { const parts = artifactName.split('.') let seen = parts[0] const art = csn.definitions[seen] // First step is not a namespace (we faked those in the CSN) // No subsequent step can be a namespace then if (art && art.kind !== 'namespace' && art.kind !== 'context') return null for (let i = 1; i < parts.length; i++) { // This was definitely a namespace so far const previousArtifactName = seen seen = `${seen}.${parts[i]}` // This might not be - if it isn't, return the result. const currentArtifact = csn.definitions[seen] if (currentArtifact && currentArtifact.kind !== 'namespace' && currentArtifact.kind !== 'context') return previousArtifactName } // We came till here - so the full artifactName is a namespace return artifactName }