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