@kumologica/builder
Version:
Kumologica build and deploy module
450 lines (400 loc) • 11.6 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { validateKumologicaFlow } = require('../code')
function genLambdaJs(flowFileName) {
return `
'use strict';
const { LambdaFlowBuilder } = require('/runtime');
const lambdaFlow = new LambdaFlowBuilder('${flowFileName}');
exports.handler = lambdaFlow.handler;
`;
}
function genAzureFunctionJs(flowFileName) {
return `
'use strict';
const { AzureFuncFlowBuilder } = require('/runtime');
const azureFuncFlow = new AzureFuncFlowBuilder('${flowFileName}');
exports.handler = azureFuncFlow.handler;
`;
}
function getBuildDir(projectDir) {
return path.join(projectDir, 'build');
}
function genFlowJson() {
return String.raw`[
{
"id": "main.flow",
"type": "tab",
"label": "main",
"disabled": false,
"info": ""
},
{
"id": "test.flow",
"type": "tab",
"label": "test",
"disabled": false,
"info": ""
},
{
"id": "eacbb9ef.69d5c8",
"type": "EventListener",
"z": "main.flow",
"name": "GET /hello",
"provider": "aws",
"eventSource": "api",
"dynamodbOperation": "",
"apiMethod": "get",
"apiUrl": "/hello",
"albMethod": "any",
"albUrl": "",
"bucketName": "",
"event": "",
"x": 120,
"y": 160,
"wires": [
[
"5eb3cd61.894f04"
]
],
"caname": "event-handler",
"category": "general"
},
{
"id": "5eb3cd61.894f04",
"type": "Logger",
"z": "main.flow",
"name": "Log",
"level": "INFO",
"message": "Request received",
"x": 310,
"y": 160,
"wires": [
[
"b5fc6ccd.17dd7"
]
],
"caname": "logger",
"category": "logging"
},
{
"id": "b5fc6ccd.17dd7",
"type": "EventListener-End",
"z": "main.flow",
"name": "Success",
"statusCode": "200",
"headers": {
"Content-Type": "application/json"
},
"payload": "{\"hello\": \"world\"}",
"x": 520,
"y": 160,
"wires": [],
"caname": "eventlistenerend",
"category": "general"
},
{
"id": "b970b7d2.5454c8",
"type": "Assertion",
"z": "test.flow",
"name": "check response",
"selector": "jsonBody",
"property": "hello",
"comparison": "equals",
"value": "world",
"x": 265,
"y": 160,
"wires": [
[
"e0721cc7.653eb"
]
],
"caname": "test-assertion",
"category": "testing"
},
{
"id": "e0721cc7.653eb",
"type": "TestCaseEnd",
"z": "test.flow",
"name": "TestCaseEnd",
"x": 425,
"y": 160,
"wires": [],
"caname": "test-case-end",
"category": "testing"
},
{
"id": "e8fb91dc.46c24",
"type": "HTTPTestCase",
"z": "test.flow",
"name": "HTTPTestCase",
"method": "GET",
"path": "/hello",
"headers": {
"Accept": "application/json"
},
"authtype": "none",
"secUser": "",
"secPassword": "",
"secToken": "",
"payload": "",
"x": 122.5,
"y": 160,
"wires": [
[
"b970b7d2.5454c8"
]
],
"caname": "http-test-case",
"category": "testing"
}
]`;
}
function sanitizeResourceName(name) {
return name
.trim()
.replace(/^@/, '')
.replace(/[^a-zA-Z0-9]/g, '')
.substr(0, 140);
}
function sanitizeLambdaName(name) {
return name
.replace('.json', '')
.trim()
.replace(/^@/, '')
.replace(/[^a-zA-Z0-9-]/g, '-')
.substr(0, 140);
}
function sanitizeAzureFuntionName(name) {
return name
.replace('.json', '')
.trim()
.replace(/^@/, '')
.replace(/[^a-zA-Z0-9]/g, '')
.toLowerCase();
}
function sanitizePackageJsonDescription(description) {
return description.replace(/\n/g, '\\n').replace(/"/g, '\\"');
}
function sanitizePackageJsonName(name) {
return sanitizeLambdaName(name).toLowerCase();
}
function genPackageJson(projectName, lambdaFileName, flowFileName, runtimeVersion, name, version, description, author, license) {
return `{
"name": "${sanitizePackageJsonName(name || projectName)}",
"version": "${version || "1.0.0"}",
"description": "${sanitizePackageJsonDescription(description || "Kumologica flow")}",
"main": "${lambdaFileName}",
"files": [
"${lambdaFileName}",
"${flowFileName}",
"node_modules/**/*"
],
"scripts": {
},
"keywords": [],
"author": "${author || ""}",
"license": "${license || "ISC"}",
"dependencies": {
"/runtime": "${runtimeVersion}"
},
"devDependencies": {
"-sdk/client-cloudwatch-events": "^3.549.0",
"-sdk/client-dynamodb": "^3.540.0",
"-sdk/client-lambda": "^3.549.0",
"-sdk/client-rekognition": "^3.549.0",
"-sdk/client-s3": "^3.550.0",
"-sdk/client-ses": "^3.549.0",
"-sdk/client-sns": "^3.549.0",
"-sdk/client-sqs": "^3.549.0",
"-sdk/client-ssm": "^3.549.0",
"-sdk/client-sts": "^3.540.0",
"-sdk/lib-dynamodb": "^3.540.0"
}
}
`;
}
function createFile(baseDir, fileName, content) {
fs.outputFileSync(path.join(baseDir, fileName), content, 'utf-8');
}
function createFileFullPath(pathFileName, content) {
fs.outputFileSync(pathFileName, content, 'utf-8');
}
function findFlowFile(projectDir) {
return fs.readdirSync(projectDir).find(f => {
return (
f.endsWith('.json') &&
f != 'template.json' &&
f !== 'package.json' &&
f !== 'package-lock.json' &&
f !== 'config.json' &&
loadAndValidateFlowFile(projectDir, f)
);
})
}
function loadAndValidateFlowFile(projectDir, file) {
let f = fs.readFileSync(path.join(projectDir, file));
let content;
try {
content = JSON.parse(f);
}catch(e){
content = ''
}
let response = validateKumologicaFlow(content);
return response.valid;
}
function loadJsonFile(file) {
return JSON.parse(fs.readFileSync(file));
}
/**
*
* @param {*} projectName
* @param {*} projectBasedir
* @returns projectDir - absolute path of the path location for the new project
*/
function generateProjectCode(directory, flowFilename) {
const lambdaFilename = 'lambda.js';
// create `lambda.js` file
createFile(directory, lambdaFilename, genLambdaJs(flowFilename));
}
function generateAzureProjectCode(directory, flowFilename) {
const lambdaFilename = 'index.js';
// create `lambda.js` file
createFile(directory, lambdaFilename, genAzureFunctionJs(flowFilename));
}
/**
*
* @param {*} options { basePath, projectName, runtimeVersion}
*/
function generateBoilerplateProject(options) {
const { basePath, projectName, runtimeVersion } = options;
generateInitialCode(basePath, projectName, undefined, runtimeVersion);
}
// function creates initial flow and package.json
function generateInitialCode(projectBasedir, projectName, flow, runtimeVersion) {
const defaultFlowFilename = `${projectName}-flow.json`;
const projectDir = path.join(projectBasedir, projectName);
// create the project directory
if (fs.existsSync(projectDir) && projectName !== '__untitled__') {
throw new Error(`"${projectDir}" directory already exists`)
} else {
fs.mkdirpSync(projectDir);
// create `{project}-flow.json` file
createFile(projectDir, defaultFlowFilename, flow ? flow : genFlowJson());
// create `package.json` file
createFile(
projectDir,
'package.json',
genPackageJson(projectName, 'lambda.js', defaultFlowFilename, runtimeVersion)
);
return projectDir;
}
}
function findTestCasesFromFlow(flowFilePath) {
const flowFileData = fs.readFileSync(flowFilePath, 'utf-8');
const flowFileJson = JSON.parse(flowFileData);
return flowFileJson.filter((node) => node.type === 'TestCase' || node.type === 'HTTPTestCase');
}
async function removeSymlinks(directory, recursively = false) {
const fsp = require("fs").promises;
console.log(`Checking for symlinks in: ${directory}`);
try {
const files = await fsp.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stats = await fsp.lstat(filePath);
if (stats.isSymbolicLink()) {
console.warn(`Removing symlink: ${filePath}`);
await fsp.unlink(filePath);
} else if (stats.isDirectory() && recursively) {
await removeSymlinks(filePath); // Recursively check subdirectories
}
}
} catch (err) {
console.error(`Error scanning directory: ${err.message}`);
}
}
async function zipDirectory(buildDir, zipFilePath) {
const yazl = require('yazl');
console.log('Zipping ...');
// Ensure output directory exists
await fs.ensureDir(path.dirname(zipFilePath));
return new Promise((resolve, reject) => {
const zipfile = new yazl.ZipFile();
const output = fs.createWriteStream(zipFilePath);
output.on('close', () => {
console.log(`Archive created: ${zipFilePath}`);
resolve();
});
output.on('error', reject);
zipfile.outputStream.pipe(output);
// Recursively walk the directory
(function addDirContents(dir, relPath = '') {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const relItemPath = path.join(relPath, item);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
addDirContents(fullPath, relItemPath);
} else {
zipfile.addFile(fullPath, relItemPath);
}
}
})(buildDir);
// End the zip file (Zip64 enabled by default in yazl)
zipfile.end();
});
}
async function extractEntryFromZip(zipFilePath, entryName, destDir) {
const yauzl = require('yauzl');
return new Promise((resolve, reject) => {
yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => {
if (err) return reject(err);
zipfile.readEntry();
zipfile.on('entry', (entry) => {
if (entry.fileName === entryName) {
zipfile.openReadStream(entry, (err, readStream) => {
if (err) return reject(err);
const destPath = path.join(destDir, entryName);
readStream.pipe(fs.createWriteStream(destPath));
readStream.on('end', () => {
zipfile.close();
resolve();
});
});
} else {
zipfile.readEntry();
}
});
zipfile.on('end', () => {
reject(new Error(`Entry ${entryName} not found in zip file.`));
});
});
});
}
// Usage:
//
// Run cleanup
//removeSymlinks("./node_modules")
// .then(() => console.log("✅ Symlink cleanup complete."))
// .catch(console.error);
module.exports = {
generateProjectCode,
generateAzureProjectCode,
generateBoilerplateProject,
generateInitialCode,
createFile,
createFileFullPath,
findFlowFile,
loadJsonFile,
sanitizeLambdaName,
sanitizeAzureFuntionName,
sanitizeResourceName,
getBuildDir,
findTestCasesFromFlow,
removeSymlinks,
genPackageJson,
zipDirectory,
extractEntryFromZip
};