@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
642 lines (584 loc) • 30.7 kB
JavaScript
const fs = require('fs')
const path = require('path')
const cds = require('../../../cds')
const { compileToHdbMigration, getJournalEntities, getMigrationTableName } = require('./2migration')
const InternalBuildPlugin = require('../internalBuildPlugin')
const { BuildError, relativePaths, BuildMessage, hasOptionValue, getFiles, getWorkspacePaths } = require('../../util')
const { OUTPUT_MODE_NONE, OUTPUT_MODE, CONTENT_PACKAGE_JSON, CONTENT_HDBTABLEDATA, CSV_FILE_DETECTION,
CONTENT_ENV, CONTENT_DEFAULT_ENV_JSON, CONTENT_NODE_MODULES, OUTPUT_MODE_RESULT, CONTINUE_UNRESOLVED_SCHEMA_CHANGES,
CSV_FILE_TARGET, OPTION_WS } = require('../../constants')
const { WARNING } = InternalBuildPlugin
const DEFAULT_COMPILE_DEST_FOLDER = path.normalize("src/gen")
const FILE_EXT_CSV = ".csv"
const FILE_EXT_HDBTABLEDATA = ".hdbtabledata"
const FILE_EXT_HDBTABLE = ".hdbtable"
const FILE_EXT_HDBMIGRATIONTABLE = ".hdbmigrationtable"
const FILE_NAME_HDICONFIG = ".hdiconfig"
const FILE_NAME_HDINAMESPACE = ".hdinamespace"
const FILE_NAME_PACKAGE_JSON = "package.json"
const PATH_LAST_DEV_CSN = "last-dev/csn.json"
const FILE_NAME_UNDEPLOY_JSON = "undeploy.json"
const DEPLOY_FORMAT = "deploy-format"
const DEBUG = cds.debug('cli|build')
class HanaBuildPlugin extends InternalBuildPlugin {
init() {
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
this._result = {
dest: this.task.dest,
hana: new Set()
}
}
this.task.options.compileDest = path.resolve(this.task.dest, this.task.options.compileDest || DEFAULT_COMPILE_DEST_FOLDER)
}
async build() {
const { src, dest } = this.task
const model = await this.model()
if (!model) {
return
}
let wsPaths = []
if(getJournalEntities(model).length || cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
// workspace paths don't include the current project root
wsPaths = await getWorkspacePaths()
}
// the order of 1 and 2 is important
// 1. compile
const hdiPlugins = await this._compileToHana(model, wsPaths)
if (!this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_NONE)) {
// 2. copy resources
if (this.hasBuildOption(CSV_FILE_DETECTION, false)) {
await this._copyResources(src, dest)
} else {
await this._copyResourcesExt(src, dest, model)
}
// create additional stuff in dest
await this._writeHdiConfig(hdiPlugins)
await this._writeHdiNamespace()
// TODO disabled as this contradicts the MTX domain concept which allows partial app deployments
//await this._writeUndeployJson()
if (!this.hasBuildOption(CONTENT_HDBTABLEDATA, false)) {
await this._compileToHdbtabledata(model)
}
if (!this.hasBuildOption(CONTENT_PACKAGE_JSON, false)) {
await this._writePackageJson()
}
// copy native hana artifacts from other workspaces including .hdbmigrationtable files defined for
// common model artifacts that are not part of the current project
if (wsPaths.length || cds.cli.options?.[OPTION_WS] || hasOptionValue(this.task.options[OPTION_WS], true)) {
// first is winning, e.g. a .hdiconfig file might exist in multiple workspaces
const wsFiles = (await this._getWorkspaceFiles(wsPaths)).reduce((acc, src) => {
const { filePath } = src;
if (!acc.some(file => file.filePath === filePath)) {
acc.push(src);
}
return acc;
}, []);
// copy artifacts to the src/gen folder in inplace mode
const dest = path.join(this.task.dest, this.isStagingBuild() ? 'src' : 'src/gen')
await Promise.all(
wsFiles.map((src) => {
const { dir, filePath } = src
return this.copy(path.join(dir, filePath)).to(path.join(dest, filePath))
})
)
}
}
return this._result
}
/**
* Deletes any content that has been created in folder '#this.task.dest/src/gen' by inplace mode.
* <br>
* Note: Content created in staging build will be deleted by the #BuildTaskEngine itself.
*/
async clean() {
if (this.isStagingBuild()) {
return super.clean()
}
return fs.promises.rm(this.task.options.compileDest, { force: true, recursive: true })
}
/**
* Copies all files located at <src> (except HANA artifacts not contained in <db>/src/**) to the folder <dest>.
* '*.csv' files are read based on the corresponding CDS model file location and copied as flat list into folder '<dest>/src/gen>'.
*
* @param {string} src
* @param {string} dest
* @param {Object} model
*/
async _copyResourcesExt(src, dest, model) {
const resources = Object.keys(await cds.deploy.resources(model)).reverse() // reverse to get reuse resources first and app resources last
const dbSrc = path.join(src, 'src')
// 1. copy csv files into 'src/gen/data' or 'src/gen/csv' subfolder
if (resources.length) {
// determine the CSV folder from the location of the CSVs defined in this build task db folder, use subfolder data by default
const dbCSVs = resources.filter(res => res.startsWith(src) && !res.startsWith(dbSrc))
let csvFolder = this.getBuildOption(CSV_FILE_TARGET)
if (csvFolder && !["data", "csv"].includes(csvFolder)) {
throw new BuildError("Invalid value for option 'csvFileTarget'. Valid values are 'data' or 'csv'.")
}
if (!csvFolder) {
const csvPath = path.join(src, "data")
// determine subfolder name used by the application for backward compatibility
csvFolder = !dbCSVs.length || dbCSVs.some(res => res && res.startsWith(csvPath)) ? "data" : "csv"
}
// await each copy as order is important - reuse resources first and app resources last
for (const res of resources) {
// do not duplicate resources that are already contained in db/src/**
if (res && /\.csv$/.test(res) && !res.startsWith(dbSrc)) {
await this.copy(res).to(path.join(this.task.options.compileDest, csvFolder, path.basename(res)))
}
}
}
if (this.isStagingBuild()) {
let blockList = "\\.cds$|\\.csv$|\\.hdbtabledata$"
blockList += !this.hasBuildOption(CONTENT_ENV, true) ? "|\\.env($|\\..*$)" : ""
blockList += !this.hasBuildOption(CONTENT_DEFAULT_ENV_JSON, true) ? "|default-env\\.json$" : ""
blockList = new RegExp(blockList)
// 2. staging build: copy files except *.cds, .env, default-env.json, ./node_modules/**
await this.copyNativeContent(src, dest, (entry) => {
if (entry.startsWith(dbSrc)) {
return true
}
if (fs.statSync(entry).isDirectory()) {
return !/(\/|\\)node_modules(\/|\\)?$/.test(entry) ||
(/(\/|\\)node_modules(\/|\\)?$/.test(entry) && this.hasBuildOption(CONTENT_NODE_MODULES, true))
}
return !blockList.test(entry)
})
}
}
/**
* Copies the entire content of the db module located in the given <src> folder to the folder <dest>.
* '*.csv' and '*.hdbtabledata' files located in a subfolder 'data' or 'csv' will be copied to '<dest>/src/gen/data>'||'<dest>/src/gen/csv>'
*
* @param {string} src
* @param {string} dest
*/
async _copyResources(src, dest) {
const dbCsvDir = path.join(src, "csv")
const dbDataDir = path.join(src, "data")
const csvDirs = [dbCsvDir, dbDataDir]
const regexData = RegExp('\\.csv$|\\.hdbtabledata$')
if (this.isStagingBuild()) {
const regex = RegExp('\\.cds$|\\.csv$|\\.hdbtabledata$')
await this.copyNativeContent(src, dest, (entry) => {
if (fs.statSync(entry).isDirectory()) {
return !/(\/|\\)node_modules(\/|\\)?$/.test(entry)
}
return (!regex.test(entry) && entry !== cds.env.build.outputfile) ||
(regexData.test(entry) && !entry.startsWith(dbCsvDir) && !entry.startsWith(dbDataDir))
})
}
// handle *.csv and *.hdbtabledata located in '<dbSrc>/data' and '<dbSrc>/csv' folder,
// subfolders are not supported
const allFiles = csvDirs.reduce((acc, csvDir) => {
return acc.concat(getFiles(csvDir, (entry) => {
if (fs.statSync(entry).isDirectory()) {
return false
}
return regexData.test(entry)
}))
}, [])
return Promise.all(allFiles.map((file) => {
return this.copy(file).to(path.join(this.task.options.compileDest, path.relative(src, file)))
}))
}
/**
* Generates *.hdbtabledata files in folder '#this.task.dest/src/gen' from *.csv files located in '#this.task.dest/src/**' folder.
* The generated *.hdbtabledata files will link to their *.csv counterparts using relative links. The *.csv files have either
* already been defined in the 'src' folder or they have been copied to '#this.task.dest/src/gen/**' folder if they have been
* created outside 'src' folder. If custom *.hdbtabledata files are found nothing is generated for this particular folder.
* <br>
* Note: *.csv and *.hdbtabledata need to be copied to '#this.task.dest/src/gen**' if required before this method is called.
* In inplace mode dest folder is refering to src folder.
*
* @param {object} model compiled csn
*/
async _compileToHdbtabledata(model) {
const tableDataDirs = new Set()
const destSrcDir = path.join(this.task.dest, "src")
const allCsvFiles = getFiles(destSrcDir, (entry) => {
if (fs.statSync(entry).isDirectory()) {
return true
}
if (/\.hdbtabledata$/.test(entry)) {
tableDataDirs.add(path.dirname(entry))
}
return /\.csv$/.test(entry)
})
if (allCsvFiles.length > 0) {
const csvDirs = allCsvFiles.map(path.dirname).reduce((dirs, dir) => {
if (!tableDataDirs.has(dir) && !dirs.includes(dir)) { // exclude any dir where a tabledata is present
dirs.push(dir)
}
return dirs
}, [])
// ODM csv data comes with license comments, so strip these
if (!this.hasBuildOption("stripCsvComments", false)) {
await this._stripCsvComments(allCsvFiles)
}
const promises = []
const options = { ...this.options(), messages: this.messages, dirs: csvDirs, baseDir: this.task.options.compileDest }
const toHdbtabledata = cds.compile.to.hdbtabledata ?? require(path.join(cds.home, 'bin/build/provider/hana/2tabledata')) // cds@6 compatibility
const tableDataResult = await toHdbtabledata(model, options)
for (let [tableData, { file, csvFolder }] of tableDataResult) {
// create .hdbtabledata side-by-side if .csv is contained in 'src/gen/**' subfolder
// otherwise create in 'src/gen'
let tableDataPath = csvFolder.startsWith(this.task.options.compileDest) ? csvFolder : this.task.options.compileDest
tableDataPath = path.join(tableDataPath, file)
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
this._result.hana.add(path.relative(this.task.dest, tableDataPath))
}
promises.push(this.write(tableData).to(tableDataPath))
}
await Promise.all(promises)
}
}
async _stripCsvComments(csvFiles) {
// Note: modification of csv files is only allowed for files located in the compile destination folder,
// meaning having their origin location at db/data/* or db/csv/*
const stripComments = cds.utils.csv?.stripComments ?? require(path.join(cds.home, 'bin/build/csv-reader')).stripComments // cds@6 compatibility
for (const file of csvFiles) {
if (this.isStagingBuild() || file.startsWith(this.task.options.compileDest)) {
await stripComments(file)
}
}
}
/**
* Creates the hana artifacts from the given csn model and writes the files to the folder '<dest>/src/gen'.
*
* @param {object} model The compiled csn model
*/
async _compileToHana(model, wsPaths) {
// compile to old format (.hdbcds) or new format (.hdbtable / .hdbview)
const format = this.getBuildOption(DEPLOY_FORMAT) || cds.env.requires.db?.[DEPLOY_FORMAT] || cds.env.hana?.[DEPLOY_FORMAT]
if (!cds.compile.to[format]) {
return Promise.reject(new Error(`Invalid deploy-format defined: ${format}`))
}
if (cds.env.features.journal === false || format === 'hdbcds') {
// compatibility with existing cds.compile.to.hdbtable plugins cds < 8.0.0
return await this._compileToHdb(model, format)
} else {
return await this._compileToHdbmigration(model, format, wsPaths)
}
}
async _compileToHdb(model, format) {
const hdiPlugins = new Set()
const relDest = path.relative(this.task.dest, this.task.options.compileDest)
// enforces sqlNames option for compiler in tests
const options = { ...this.options(), sql_mapping: cds.env.sql.names }
const result = cds.compile.to[format](model, options)
const promises = []
for (const [content, key] of result) {
const suffix = key.suffix || path.extname(key.file)
const file = key.file ? key.file : key.name + key.suffix
hdiPlugins.add(suffix)
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
this._result.hana.add(path.join(relDest, file))
}
promises.push(this.write(content).to(path.join(this.task.options.compileDest, file)))
if (suffix === FILE_EXT_HDBTABLE) {
const name = key.name || path.parse(key.file).name
const dbSrcDir = path.join(this.task.src, "src")
// issue an error in case a .hdbmigrationtable file already exists
if (fs.existsSync(path.join(dbSrcDir, name + FILE_EXT_HDBMIGRATIONTABLE))) {
const relDbSrcDir = path.relative(cds.root, dbSrcDir)
const relDbDestDir = path.relative(cds.root, this.task.options.compileDest)
throw new BuildError(`Multiple files exist defining the same HANA artifact - [${path.join(relDbSrcDir, name + FILE_EXT_HDBMIGRATIONTABLE)}, ${path.join(relDbDestDir, file)}].\nEither annotate the model entity using @cds.persistence.journal or undeploy the file [${path.join('src', name + FILE_EXT_HDBMIGRATIONTABLE)}] using an undeploy.json file.`)
}
}
}
await Promise.all(promises)
return hdiPlugins
}
async _compileToHdbmigration(model, format, wsPaths) {
const hdiPlugins = new Set()
const relDestDir = path.relative(this.task.dest, this.task.options.compileDest)
const relDbDestDir = path.relative(cds.root, this.task.options.compileDest)
const dbSrcDir = path.join(this.task.src, "src")
const relDbSrcDir = path.relative(cds.root, dbSrcDir)
const lastDevCsnFolder = PATH_LAST_DEV_CSN
const lastDevCsnDir = path.join(this.task.src, lastDevCsnFolder)
let lastDev = null
const promises = []
const migrationTableFiles = []
if (fs.existsSync(lastDevCsnDir)) {
lastDev = JSON.parse((await fs.promises.readFile(lastDevCsnDir, 'utf-8')).toString())
}
// enforces sqlNames option for compiler in tests, pass options from cds env, ensures that the correct format is taken
const options = { ...this.options(), messages: this.messages, sql_mapping: cds.env.sql.names, "deploy-format": format }
const { definitions, afterImage } = await compileToHdbMigration(model, lastDev, dbSrcDir, options)
let validationError
// determine the migration tables originating from a different workspaces
const journalEntities = getJournalEntities(model)
const wsMigrationTables = new Set()
if (journalEntities.length) {
for (const entity of journalEntities) {
if (entity['$location']?.file && wsPaths.some(wsPath => path.resolve(cds.root, entity['$location'].file).startsWith(wsPath))) {
wsMigrationTables.add(getMigrationTableName(entity, model))
}
}
}
for (const { name, suffix, content, changed } of definitions) {
hdiPlugins.add(suffix)
const fileName = name + suffix
if (suffix === FILE_EXT_HDBMIGRATIONTABLE) {
// do not create .hdbmigrationtable files originating from a different workspace
// the --ws build option will copy these artifacts to either gen/db/src or db/src/gen
if (!wsMigrationTables.has(name)) {
migrationTableFiles.push(path.join(relDbSrcDir, fileName))
if (changed) {
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
this._result.hana.add(path.join("src", fileName))
}
promises.push(this.write(content).to(path.join(dbSrcDir, fileName)))
} else {
DEBUG?.(`no change, keep existing ${fileName}`)
}
} else {
DEBUG?.(`skip generation of migration table [${fileName}] as it originates from a different workspace`)
}
} else {
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
this._result.hana.add(path.join(relDestDir, fileName))
}
promises.push(this.write(content).to(path.join(this.task.options.compileDest, fileName)))
if (suffix === FILE_EXT_HDBTABLE) {
// issue an error in case a .hdbmigrationtable file already exists
if (fs.existsSync(path.join(dbSrcDir, name + FILE_EXT_HDBMIGRATIONTABLE))) {
validationError = new BuildError(`Multiple files exist defining the same HANA artifact - [${path.join(relDbSrcDir, name + FILE_EXT_HDBMIGRATIONTABLE)}, ${path.join(relDbDestDir, fileName)}].\nEither annotate the model entity using @cds.persistence.journal or undeploy the file [${path.join('src', name + FILE_EXT_HDBMIGRATIONTABLE)}] using an undeploy.json file.`)
}
}
}
}
// The last-dev CSN shall only be updated, if all .hdbmigrationtable files could be successfully updated.
// Of course, .hdbmigrationtable files already written would need to be manually reverted before the
// cds build command is again executed.
await Promise.all(promises)
try {
if (validationError) {
throw validationError
}
await this._validateMigrationTableFiles()
} finally {
// update last-dev CSN version
if (afterImage) {
if (!HanaBuildPlugin._toEqualIgnoreMeta(lastDev, afterImage)) {
await this.write(afterImage).to(lastDevCsnDir)
}
// add src/.hdiconfig if not existing
if (migrationTableFiles.length > 0 && !fs.existsSync(path.join(dbSrcDir, FILE_NAME_HDICONFIG))) {
const template = await HanaBuildPlugin._readTemplateAsJson('.hdiconfig-cloud')
await this.write(template).to(path.join(dbSrcDir, FILE_NAME_HDICONFIG))
}
}
}
return hdiPlugins
}
async _writePackageJson() {
const pkgJson = path.join(this.task.src, "package.json")
const exists = fs.existsSync(pkgJson)
if (exists) {
DEBUG?.(`skip create [${relativePaths(cds.root, pkgJson)}], already existing`)
}
if (this.isStagingBuild() && !exists) {
const content = await HanaBuildPlugin._readTemplateAsJson(FILE_NAME_PACKAGE_JSON)
await this.write(content).to(path.join(this.task.dest, FILE_NAME_PACKAGE_JSON))
}
}
/**
* Create .hdiconfig file in <dest>src/gen folder of db module.
*/
async _writeHdiConfig(compileHdiPlugins) {
const DEFAULT_HDI_PLUGINS = [FILE_EXT_CSV, FILE_EXT_HDBTABLEDATA, FILE_EXT_HDBTABLE, ".hdbview", ".hdbindex", ".hdbconstraint", ".hdbcalculationview"]
const undeployHdiPlugins = await this._readTypesFromUndeployJson() // see CAP issue #6222
const hdiPlugins = new Set([...DEFAULT_HDI_PLUGINS, ...undeployHdiPlugins, ...compileHdiPlugins])
let hdiConfigPath
// do not create the .hdiconfig file in 'gen/db' root folder for now
// if (this.isStagingBuild()
// && !fs.existsSync(path.join(this.task.dest, FILE_NAME_HDICONFIG))
// && !fs.existsSync(path.join(this.task.dest, 'cfg', FILE_NAME_HDICONFIG))) {
// // in 'gen/db' if none exists
// // ensures correct deployment of all artifacts including HANA native artifacts
// // without the need of creating a static .hdiconfig file in the 'db' folder
// // add all known plugins
// const defaultHdiConfig = await HanaBuildPlugin._readTemplateAsJson('.hdiconfig-cloud')
// for (const plugin in defaultHdiConfig['file_suffixes']) {
// hdiPlugins.add('.' + plugin)
// }
// hdiConfigPath = path.join(this.task.dest, FILE_NAME_HDICONFIG)
// } else {
// in 'gen/db/src/gen'
// ensures correct deployment of generated and undeploy.json artifacts
// a static .hdiconfig file in 'db' folder is required for any HANA native artifacts
hdiConfigPath = path.join(this.task.options.compileDest, FILE_NAME_HDICONFIG)
// }
const template = await HanaBuildPlugin._readTemplateAsJson('.hdiconfig-all')
let content = {
'file_suffixes': {}
}
// only use the required subset as SAP HANA cloud does not support all hdiPlugins
for (const key in template['file_suffixes']) {
if (hdiPlugins.has('.' + key)) {
content['file_suffixes'][key] = template['file_suffixes'][key]
}
}
if (Object.keys(content['file_suffixes']).length !== hdiPlugins.size) {
this.pushMessage(`'HANA database plugin not found for file suffix [${Array.from(hdiPlugins).join(',')}]`)
}
await this.write(content).to(hdiConfigPath)
}
/**
* Create .hdinamespace file in <dest>src/gen folder of db module.
*/
async _writeHdiNamespace() {
// see issue #64 - add .hdinamespace file to prevent HDI from adding gen/ folder to the namespace.
const hdiNamespace = path.join(this.task.options.compileDest, FILE_NAME_HDINAMESPACE)
const content = await HanaBuildPlugin._readTemplateAsJson(FILE_NAME_HDINAMESPACE)
return await this.write(content).to(hdiNamespace)
}
/**
* Create undeploy.json file in <dest> folder of db module.
*/
async _writeUndeployJson() {
if (this.isStagingBuild()) {
// see issue #64 - add .hdinamespace file to prevent HDI from adding gen/ folder to the namespace.
const undeployJsonDest = path.join(this.task.dest, FILE_NAME_UNDEPLOY_JSON)
const undeployJsonSrc = path.join(this.task.src, FILE_NAME_UNDEPLOY_JSON)
const templateEntries = await HanaBuildPlugin._readTemplateAsJson(FILE_NAME_UNDEPLOY_JSON)
let newEntries = []
if (fs.existsSync(undeployJsonSrc)) {
newEntries = await JSON.parse((await fs.promises.readFile(undeployJsonSrc, 'utf-8')).toString())
newEntries = Array.isArray(newEntries) ? newEntries : []
templateEntries.forEach(entry => {
if (!newEntries.includes(entry)) {
newEntries.push(entry)
}
})
} else {
newEntries = templateEntries
}
// formatted output
let content = '[\n'
for (let i = 0; i < newEntries.length; i++) {
content += ` "${newEntries[i]}"${i + 1 < newEntries.length ? ',' : ''}\n`
}
content += ']'
await this.write(content).to(undeployJsonDest)
}
}
async _readTypesFromUndeployJson() {
const hdiPlugins = new Set()
const file = path.join(this.task.src, "undeploy.json")
if (fs.existsSync(file)) {
const undeployList = JSON.parse((await fs.promises.readFile(file)).toString(), 'utf-8')
if (Array.isArray(undeployList)) {
const hdiconfig = await HanaBuildPlugin._readTemplateAsJson('.hdiconfig-all')
const keys = new Set(Object.keys(hdiconfig['file_suffixes']).map(key => '.' + key))
undeployList.forEach(entry => {
const extName = path.extname(entry)
if (extName && !hdiPlugins.has(extName)) {
if (keys.has(extName)) {
hdiPlugins.add(extName)
} else {
this.pushMessage(`Ignoring invalid entry '${entry}' in undeploy.json file`, WARNING)
}
}
})
}
}
return hdiPlugins
}
async _validateMigrationTableFiles() {
const dbSrcDir = path.join(this.task.src, "src")
const migrationTableFiles = getFiles(dbSrcDir, (res) => {
return fs.statSync(res).isFile() && path.extname(res) === FILE_EXT_HDBMIGRATIONTABLE
})
if (migrationTableFiles.length > 0) {
const parser = require('./migrationtable')
const resolutionMessages = []
await Promise.all(migrationTableFiles.map(async file => {
try {
const tableModel = await parser.read(file)
if (/^>>>>>/m.test(tableModel.migrations.toString())) {
// as this is not a build error, we do not abort cds build, instead only log as error
resolutionMessages.push(new BuildMessage(`Manual resolution required for file ${path.relative(cds.root, file)}. Check migration version content for further details.`))
}
} catch (e) {
throw new Error(`${path.relative(cds.root, file)}: ${e.toString()}`)
}
}))
if (resolutionMessages.length) {
// REVISIT: undocumented, we may want to delete in future versions
if (!this.hasBuildOption(CONTINUE_UNRESOLVED_SCHEMA_CHANGES, true)) {
throw new BuildError('Current model changes require manual resolution', resolutionMessages)
}
// adding to the task's message list
resolutionMessages.forEach(m => this._messages.push(m))
}
}
}
/**
* Returns all hana native artifacts located in db/src/** workspace folders.
* @param {Array} workspacePaths Array of workspace paths, 'cds.root' is excluded from the list
* @returns {Array} Array of workspace files, each entry contains the db src directory,
* the relative file path and the file name.
*/
async _getWorkspaceFiles(workspacePaths) {
if (!workspacePaths || !workspacePaths.length) {
return []
}
const dbPaths = new Set()
workspacePaths.forEach(workspacePath => {
// skip workspace matching the current project if cds build is executed in a workspace folder
if (cds.root !== workspacePath) {
const dbSrc = path.join(workspacePath, 'db/src')
const dbCfg = path.join(workspacePath, 'db/cfg')
if (fs.existsSync(dbSrc)) {
dbPaths.add(dbSrc)
}
if (fs.existsSync(dbCfg)) {
dbPaths.add(dbCfg)
}
}
})
const workspaceFiles = [...dbPaths].reduce((acc, src) => {
const files = getFiles(src, (entry) => {
if (entry.match(/(\/|\\)gen(\/|\\)?$/)) {
// skip gen folder content
return false
}
return true
})
files.forEach(file => acc.push({ dir: src, filePath: path.relative(src, file), fileName: path.basename(file) }))
return acc
}, [])
return workspaceFiles
}
_hasJournalDefinitions(model) {
return Object.values(model.definitions).some(def => def['@cds.persistence.journal'])
}
static async _readTemplateAsJson(template) {
const content = await fs.promises.readFile(path.join(__dirname, 'template', template), 'utf-8')
return JSON.parse(content.toString())
}
static _toEqualIgnoreMeta(csn1, csn2) {
function toString(csn) {
return JSON.stringify(csn, (k, v) => {
if (v?.creator) {
// make sure it's the compiler meta tag
if (k === 'meta' && v.creator?.startsWith('CDS Compiler'))
return
}
return v
})
}
if (csn1 === csn2) {
return true
}
if (!csn1 || !csn2) {
return false
}
return toString(csn1) === toString(csn2)
}
}
module.exports = HanaBuildPlugin