diff --git a/postgres/cds-plugin.js b/postgres/cds-plugin.js index b91593583..7213c86db 100644 --- a/postgres/cds-plugin.js +++ b/postgres/cds-plugin.js @@ -65,15 +65,80 @@ cds.build?.register?.('postgres', class PostgresBuildPlugin extends cds.build.Pl promises.push(this.write(cds.compile.to.json(model)).to(path.join('db', 'csn.json'))) - let data - if (fs.existsSync(path.join(this.task.src, 'data'))) { - data = 'data' - } else if (fs.existsSync(path.join(this.task.src, 'csv'))) { - data = 'csv' - } - if (data) { - promises.push(this.copy(data).to(path.join('db', 'data'))) + // csvFileDetection (default on, for parity with the HANA build task) collects initial + // data from all model sources - app-local db/data AND every reuse module's db/data - so + // the deployer artifact matches `cds deploy` from source. When disabled, only the + // app-local data/csv folder is copied (legacy behaviour). + const csvFileDetection = this.task.options?.csvFileDetection ?? true + if (csvFileDetection && typeof cds.deploy?.resources === 'function') { + for (const [dest, sources] of await collectInitialData(model)) { + const to = path.join('db', 'data', dest) + if (sources.length === 1) { + promises.push(this.copy(sources[0]).to(to)) + } else { + promises.push(this.write(mergeCsvFiles(sources)).to(to)) + } + } + } else { + let data + if (fs.existsSync(path.join(this.task.src, 'data'))) { + data = 'data' + } else if (fs.existsSync(path.join(this.task.src, 'csv'))) { + data = 'csv' + } + if (data) { + promises.push(this.copy(data).to(path.join('db', 'data'))) + } } return Promise.all(promises) } }) + +// Discover initial-data resources across app + reuse modules, grouped by their +// destination file name. Files that resolve to the same destination (e.g. a code list +// extended by a reuse module and the consumer) are merged; init.js/ts is skipped as it +// cannot be reproduced from the artifact's db/data folder. +async function collectInitialData (model) { + const resources = await cds.deploy.resources(model) + const byDest = new Map() + for (const [file, entity] of Object.entries(resources)) { + if (entity === '*') continue // init.js/ts + const dest = path.basename(file) + const group = byDest.get(dest) + if (group) group.push(file) + else byDest.set(dest, [file]) + } + return byDest +} + +// Merge several CSV files for the same entity into the union of their rows under a unified +// header. Files are discovered reuse-module-first, so base rows precede consumer rows. +function mergeCsvFiles (files) { + const columns = [] + const seen = new Set() + const records = [] + for (const file of files) { + const [header, ...rows] = cds.parse.csv(fs.readFileSync(file, 'utf8')) + if (!header) continue + for (const column of header) if (!seen.has(column)) { seen.add(column); columns.push(column) } + for (const row of rows) { + const record = {} + header.forEach((column, i) => { record[column] = row[i] }) + records.push(record) + } + } + const lines = [columns.map(csvEscape).join(',')] + const emitted = new Set() + for (const record of records) { + const line = columns.map(column => csvEscape(record[column] ?? '')).join(',') + if (emitted.has(line)) continue + emitted.add(line) + lines.push(line) + } + return lines.join('\n') + '\n' +} + +function csvEscape (value) { + value = String(value) + return /[",\n\r]/.test(value) ? '"' + value.replace(/"/g, '""') + '"' : value +} diff --git a/postgres/test/cds-build.test.js b/postgres/test/cds-build.test.js index 8d897581d..af009e2b1 100644 --- a/postgres/test/cds-build.test.js +++ b/postgres/test/cds-build.test.js @@ -12,9 +12,13 @@ const genDir = path.join(workDir, 'gen') const pgDest = path.join(genDir, 'pg') const dbDest = path.join(pgDest, 'db') +const reuseWorkDir = path.join(__dirname, 'reuse-sample') +const reuseGenDir = path.join(reuseWorkDir, 'gen') + // delete the generated folder after each test afterEach(() => { if (fs.existsSync(genDir)) fs.rmSync(genDir, { recursive: true }) + if (fs.existsSync(reuseGenDir)) fs.rmSync(reuseGenDir, { recursive: true }) }) describe('cds build plugin', () => { @@ -61,4 +65,12 @@ describe('cds build plugin', () => { const pgAdapterVersion = require(path.join(__dirname,'..','package.json')).version; expect(packageJson.dependencies?.['@cap-js/postgres']).to.equal(pgAdapterVersion) }) + + test('should merge initial data of reuse modules into gen/pg/db/data', () => { + execSync('npx cds build --for postgres', { cwd: reuseWorkDir }) + const csv = fs.readFileSync(path.join(reuseGenDir, 'pg', 'db', 'data', 'my.reuse-CodeList.csv'), 'utf8') + const codes = csv.trim().split(/\r?\n/).slice(1).map(line => line.split(',')[0]) + // union of the reuse module's rows (A, B, C) and the consumer's own row (D) + expect(codes.sort()).to.eql(['A', 'B', 'C', 'D']) + }) }) diff --git a/postgres/test/reuse-sample/db/data/my.reuse-CodeList.csv b/postgres/test/reuse-sample/db/data/my.reuse-CodeList.csv new file mode 100644 index 000000000..e518296d6 --- /dev/null +++ b/postgres/test/reuse-sample/db/data/my.reuse-CodeList.csv @@ -0,0 +1,2 @@ +code,name +D,Delta diff --git a/postgres/test/reuse-sample/db/index.cds b/postgres/test/reuse-sample/db/index.cds new file mode 100644 index 000000000..0198e96c6 --- /dev/null +++ b/postgres/test/reuse-sample/db/index.cds @@ -0,0 +1,3 @@ +// The reuse module is referenced via a relative path so the fixture needs no +// node_modules; resource discovery treats its db/data folder like any dependency's. +using from '../reuse/db/schema'; diff --git a/postgres/test/reuse-sample/package.json b/postgres/test/reuse-sample/package.json new file mode 100644 index 000000000..52d3a92ba --- /dev/null +++ b/postgres/test/reuse-sample/package.json @@ -0,0 +1,15 @@ +{ + "name": "reuse-sample", + "version": "1.0.0", + "description": "A CAP project consuming a reuse module, to test the build plugin", + "dependencies": { + "@cap-js/postgres": "../../." + }, + "cds": { + "requires": { + "db": { + "kind": "postgres" + } + } + } +} \ No newline at end of file diff --git a/postgres/test/reuse-sample/reuse/db/data/my.reuse-CodeList.csv b/postgres/test/reuse-sample/reuse/db/data/my.reuse-CodeList.csv new file mode 100644 index 000000000..2723a2108 --- /dev/null +++ b/postgres/test/reuse-sample/reuse/db/data/my.reuse-CodeList.csv @@ -0,0 +1,4 @@ +code,name +A,Alpha +B,Beta +C,Gamma diff --git a/postgres/test/reuse-sample/reuse/db/schema.cds b/postgres/test/reuse-sample/reuse/db/schema.cds new file mode 100644 index 000000000..db7620086 --- /dev/null +++ b/postgres/test/reuse-sample/reuse/db/schema.cds @@ -0,0 +1,6 @@ +namespace my.reuse; + +entity CodeList { + key code : String; + name : String; +} diff --git a/postgres/test/reuse-sample/reuse/package.json b/postgres/test/reuse-sample/reuse/package.json new file mode 100644 index 000000000..6260a25f5 --- /dev/null +++ b/postgres/test/reuse-sample/reuse/package.json @@ -0,0 +1,5 @@ +{ + "name": "@cap-js-test/reuse-code-list", + "version": "1.0.0", + "description": "A reuse module shipping initial data, to test the build plugin" +} \ No newline at end of file