@sap/ams-dev
Version:
NodesJS AMS development environment
130 lines (105 loc) • 3.88 kB
JavaScript
const cds = require('@sap/cds');
const fs = require("node:fs");
const path = require("node:path");
const env = require('./capAmsDevEnv');
const AmsClient = require('../client/AmsClient');
const { compileDcl } = require('../compileDcl');
const { prepareFolders, debouncePromise } = require('../util');
let watch; // lazy loaded dependency
const LOG = cds.log('ams');
const DCL_COMPILATION_DEBOUNCE_PARAMETER = 500;
const watchers = {}; // file system watchers
const RUNTIME_COMMANDS = ["serve", "test", "watch"];
/** Entry-point for AMS dev plugin. Must only be called once to bootstrap the plugin when the CAP application starts. */
function init() {
LOG.debug('AMS Dev Plugin loaded.');
if (RUNTIME_COMMANDS.includes(cds.cli?.command)) {
prepareNewBundle();
}
if (cds.watched) {
cds.once('shutdown', () => {
for (const watcher of Object.values(watchers)) {
watcher.close();
}
});
cds.once('bootstrap', () => {
watchDclFolder();
});
}
}
/* Schedule compilation of DCL to REGO whenever a DCL file is changed. */
async function watchDclFolder() {
watch ??= require('node-watch');
const dclFolder = env.dclRoot;
await prepareFolders([dclFolder]);
watchers.dcl = watch(dclFolder, { recursive: true, filter: /\.dcl$/ }, debouncePromise(dclFileChanged, DCL_COMPILATION_DEBOUNCE_PARAMETER));
}
async function dclFileChanged(eventType, filename) {
LOG.debug(`DCL file ${filename} was ${eventType}d.`);
await prepareNewBundle(eventType, filename);
}
/** Reaction to DCL file changes. Recompiles DCL to DCN. In Hybrid mode, pushes DCL to AMS Server instead. */
async function prepareNewBundle() {
if (cds.env.profiles.includes("hybrid") && env.autoDeployDcl) {
// Hybrid testing bound to AMS Server
try {
await uploadDclToAmsServer();
} catch (e) {
LOG.error('DCL could not be uploaded to the AMS server.', e);
return;
}
} else {
compileDcn();
}
}
async function uploadDclToAmsServer() {
const iasCredentials = cds.env?.requires?.auth?.credentials;
if (iasCredentials == null) {
LOG.warn('Cannot upload AMS policies because ias credentials are missing.');
return;
}
const ams = new AmsClient(iasCredentials);
LOG.debug(`Deploying DCL policies to AMS instance ${iasCredentials.authorization_instance_id} on ${iasCredentials.url} \
(disable via requires.auth.ams.autoDeployDcl = false).`);
await ams.uploadBaseDCL(env.dclRoot, { deployerAppName: "@sap/ams-dev:CAP_plugin_hybrid_mode_deployer" });
LOG.info('DCL policies succesfully deployed to AMS server (Hybrid testing mode with requires.auth.ams.autoDeployDcl = true detected).');
}
/**
* Cleans the DCN root directory by removing all files and subdirectories except `data.json`.
* @param {string} dcnRoot - The root directory to clean.
*/
function cleanDcnRoot(dcnRoot) {
try {
// Read the directory contents
const entries = fs.readdirSync(dcnRoot, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dcnRoot, entry.name);
if (entry.isDirectory()) {
// Remove directory and all its contents recursively
fs.rmSync(fullPath, { recursive: true, force: true });
} else if (entry.isFile() && entry.name !== "data.json") {
// Remove files that are not `data.json`
fs.unlinkSync(fullPath);
}
}
} catch (err) {
if (err.code === "ENOENT") {
// Directory doesn't exist, nothing to do
} else {
console.error("Failed to clean the DCN root directory before DCL->DCN compilation:", err);
}
}
}
/** Compiles the DCL files of the application to DCN. */
async function compileDcn() {
LOG.debug("Compiling DCL to DCN.");
await cleanDcnRoot(env.dcnRoot);
const args = ["-d", env.dclRoot, "-o", env.dcnRoot];
try {
compileDcl(args);
} catch (error) {
LOG.error("DCL->DCN compilation failed:", error.stdout);
LOG.error(error.stdout);
}
}
module.exports = { init };