From 22c5d86369a6741cd8d7e685ce0b7d0b4480512c Mon Sep 17 00:00:00 2001 From: Arya Patil Date: Mon, 13 Apr 2026 23:38:16 +0530 Subject: [PATCH] Add manifest filtering support in env export --- packages/mdctl-cli/lib/env/export.js | 105 +++++++++++++-------- packages/mdctl-cli/test/lib/env/export.js | 108 ++++++++++++++++++++++ 2 files changed, 172 insertions(+), 41 deletions(-) diff --git a/packages/mdctl-cli/lib/env/export.js b/packages/mdctl-cli/lib/env/export.js index be747f5da..0298fa628 100644 --- a/packages/mdctl-cli/lib/env/export.js +++ b/packages/mdctl-cli/lib/env/export.js @@ -15,6 +15,7 @@ const fs = require('fs'), { searchParamsToObject } = require('@medable/mdctl-core-utils'), + os = require('os'), { Config, Fault } = require('@medable/mdctl-core'), ExportStream = require('@medable/mdctl-core/streams/export_stream'), ExportFileTreeAdapter = require('@medable/mdctl-export-adapter-tree'), @@ -24,9 +25,12 @@ const fs = require('fs'), exportEnv = async(input) => { const options = isSet(input) ? input : {}, - { manifest: optionsManifest } = options, - client = options.client || new Client({ ...Config.global.client, ...options }), - outputDir = options.dir || process.cwd(), + { manifest: optionsManifest } = options + +let manifest = {} + +const client = options.client || new Client({ ...Config.global.client, ...options }), + outputDir = options.dir || process.cwd(), packageFile = options.package || `${outputDir}/package.${options.format || 'json'}`, // stream = ndjson.parse(), url = new URL('/developer/environment/export', client.environment.url), @@ -44,10 +48,11 @@ const fs = require('fs'), clearOutput: options.clear }, streamTransform = new ExportStream(), - adapter = options.adapter || new ExportFileTreeAdapter(outputDir, streamOptions), + adapter = options.adapter || new ExportFileTreeAdapter(outputDir, streamOptions) // eslint-disable-next-line max-len lockUnlock = new LockUnlock(outputDir, client.environment.endpoint, client.environment.env), - memo = {}, + memo = {} + logStream = new Transform({ objectMode: true, transform(chunk, encoding, cb) { @@ -58,7 +63,7 @@ const fs = require('fs'), }) let manifestFile, - inputStream, + inputStream preExport = () => {}, postExport = () => {} @@ -72,8 +77,7 @@ const fs = require('fs'), if (!options.stream) { let pkg, - script, - manifest = {} + script const getScript = (...params) => { for (const param of params) { // eslint-disable-line no-restricted-syntax @@ -132,15 +136,28 @@ const fs = require('fs'), } } - if (optionsManifest) { - manifest = optionsManifest - } else if (pkg && pkg.manifest) { + if (optionsManifest) { + const expandedManifest = optionsManifest.startsWith('~') + ? path.join(os.homedir(), optionsManifest.slice(1)) + : optionsManifest + + manifestFile = path.isAbsolute(expandedManifest) + ? expandedManifest + : path.join(outputDir, expandedManifest) + + if (!fs.existsSync(manifestFile)) { + throw Fault.create('kNotFound', { reason: `Manifest file not found: ${manifestFile}` }) + } + + manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')) +} else if (pkg && pkg.manifest) { manifestFile = `${outputDir}/${pkg.manifest}` } else if (fs.existsSync(`${outputDir}/manifest.${options.format || 'json'}`)) { manifestFile = `${outputDir}/manifest.${options.format || 'json'}` - } + } + // console.log('Using manifest:', JSON.stringify(manifest, null, 2)) - if (fs.existsSync(manifestFile)) { + if (manifestFile && !optionsManifest && fs.existsSync(manifestFile)) { try { manifest = parseString(fs.readFileSync(manifestFile), options.format) } catch (e) { @@ -162,38 +179,44 @@ const fs = require('fs'), } return new Promise((resolve, reject) => { - const resultStream = pump(inputStream, streamTransform, logStream, adapter, async(err) => { - try { - await postExport({ - client, err, options, memo - }) - } catch (e) { - return reject(e) - } + const isConsoleAdapter = + adapter && adapter.constructor && adapter.constructor.name === 'ExportConsoleAdapter' - if (err) { - return reject(err) - } + let resultStream - if (!streamTransform.complete()) { - return reject(new Error('Export not complete!')) - } + if (isConsoleAdapter) { + resultStream = pump(inputStream, logStream, adapter, async (err) => { + if (err) return reject(err) - if (options.docs) { - console.log('Documenting env') - return Docs.generateDocumentation({ - destination: path.join(outputDir, 'docs'), - source: path.join(outputDir), - module: 'env', - }).then(() => { - resolve(resultStream) - }) - } - return resolve(resultStream) + try { + await postExport({ client, err, options, memo }) + } catch (e) { + return reject(e) + } - }) - }) + return resolve(resultStream) + }) + } else { + resultStream = pump(inputStream, streamTransform, logStream, adapter, async (err) => { + if (err) return reject(err) + + try { + await postExport({ client, err, options, memo }) + } catch (e) { + return reject(e) } -module.exports = exportEnv + if (!streamTransform.complete()) { + return reject(new Error('Export not complete!')) + } + + return resolve(resultStream) + }) + } + +}) + +} + +module.exports = exportEnv \ No newline at end of file diff --git a/packages/mdctl-cli/test/lib/env/export.js b/packages/mdctl-cli/test/lib/env/export.js index 7fd79a948..4a8fcca64 100644 --- a/packages/mdctl-cli/test/lib/env/export.js +++ b/packages/mdctl-cli/test/lib/env/export.js @@ -1,13 +1,24 @@ + /* eslint-disable import/no-extraneous-dependencies */ const { assert } = require('chai'), fs = require('fs'), path = require('path'), glob = require('glob'), rimraf = require('rimraf'), + ndjson = require('ndjson'), ExportConsoleAdapter = require('@medable/mdctl-export-adapter-console'), { Client } = require('@medable/mdctl-api'), exportEnv = require('../../../lib/env/export') +// New Helper added here +const makeClient = () => ({ + environment: { + url: 'https://localhost', + endpoint: 'localhost', + env: 'test' + } +}) + describe('Environment Export', () => { let blob, @@ -110,6 +121,8 @@ describe('Environment Export', () => { }), adapter = new ExportConsoleAdapter({ print: false }) + fs.mkdirSync(tempDir, { recursive: true }) + return exportEnv({ client, adapter, @@ -126,3 +139,98 @@ describe('Environment Export', () => { }) }) + +// New Test Section added here +describe('Environment Export -- manifest option', () => { + + let tempDir, manifestContent, capturedBody + + beforeEach(() => { + tempDir = path.join(process.cwd(), `output-manifest-test-${new Date().getTime()}`) + manifestContent = { scripts: { includes: ['*'] } } + capturedBody = null + fs.mkdirSync(tempDir, { recursive: true }) + }) + + afterEach(() => { + rimraf.sync(tempDir) + }) + + const makeCallMock = () => async(pathname, opts) => { + capturedBody = opts.body + opts.stream.write(JSON.stringify({ object: 'manifest-exports' }) + '\n') + opts.stream.end() + } + + it('resolves an absolute --manifest path and sends parsed object to API', async() => { + const manifestFile = path.join(tempDir, 'my-manifest.json') + fs.writeFileSync(manifestFile, JSON.stringify(manifestContent)) + + const client = { ...makeClient(), call: makeCallMock() } + + await exportEnv({ client, dir: tempDir, manifest: manifestFile, format: 'json' }) + + assert.deepEqual(capturedBody.manifest, manifestContent, + '--manifest absolute path should be parsed and sent as object, not as a string') + }) + + it('expands ~ in --manifest path to the home directory', async() => { + const os = require('os') + const manifestFile = path.join(tempDir, 'tilde-manifest.json') + fs.writeFileSync(manifestFile, JSON.stringify(manifestContent)) + + const relativeToCwd = path.relative(os.homedir(), manifestFile) + const tildeManifest = `~/${relativeToCwd}` + + const client = { ...makeClient(), call: makeCallMock() } + + await exportEnv({ client, dir: tempDir, manifest: tildeManifest, format: 'json' }) + + assert.deepEqual(capturedBody.manifest, manifestContent, + '--manifest with ~ should be expanded to the home directory') + }) + + it('resolves a relative --manifest path against the output dir', async() => { + const manifestFile = path.join(tempDir, 'custom-manifest.json') + fs.writeFileSync(manifestFile, JSON.stringify(manifestContent)) + + const client = { ...makeClient(), call: makeCallMock() } + + await exportEnv({ client, dir: tempDir, manifest: 'custom-manifest.json', format: 'json' }) + + assert.deepEqual(capturedBody.manifest, manifestContent, + '--manifest relative path should be resolved against outputDir and parsed') + }) + + it('sends ec__ objects manifest with env includes and 7 named objects', async() => { + const sampleManifest = { + env: { includes: ['*'] }, + object: 'manifest', + objects: [ + { includes: ['*'], name: 'ec__default_document_css' }, + { includes: ['*'], name: 'ec__document_datum' }, + { includes: ['*'], name: 'ec__document_invite' }, + { includes: ['*'], name: 'ec__document_template' }, + { includes: ['*'], name: 'ec__knowledge_check' }, + { includes: ['*'], name: 'ec__linked_field' }, + { includes: ['*'], name: 'ec__signed_document' } + ] + } + const manifestFile = path.join(tempDir, 'manifest.json') + fs.writeFileSync(manifestFile, JSON.stringify(sampleManifest)) + + const client = { ...makeClient(), call: makeCallMock() } + + await exportEnv({ client, dir: tempDir, format: 'json' }) + + assert.deepEqual(capturedBody.manifest, sampleManifest, + 'manifest with ec__ objects should be auto-discovered and sent as parsed object') + assert.strictEqual(capturedBody.manifest.object, 'manifest') + assert.strictEqual(capturedBody.manifest.objects.length, 7) + assert.isTrue( + capturedBody.manifest.objects.every(o => o.includes[0] === '*'), + 'all objects should have wildcard includes' + ) + }) + +}) \ No newline at end of file