Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: add visualise command, sql only #472

Merged
merged 1 commit into from
Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/cli/src/commands/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export async function dockerCommand(logger, command) {
const subCommand = command.arguments[0];
if (SUB_COMMANDS.indexOf(subCommand) === -1) {
logger.info(
`Unknown command: 'lbu docker ${subCommand}'. Please use one of ${SUB_COMMANDS.join(
", ",
)}`,
`Unknown command: 'lbu docker ${
subCommand ?? ""
}'. Please use one of ${SUB_COMMANDS.join(", ")}`,
);
return { exitCode: 1 };
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Usage:
- bench : lbu bench [--watch] [--verbose] [--node-arg]
- coverage : lbu coverage [--watch] [--verbose] [--any-node-arg] [-- --c8-arg]
- lint : lbu lint [--watch] [--verbose] [--any-node-arg]
- visualise : lbu visualise [sql,router] {path/to/generated/index.js}


Available script names:
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { lintCommand } from "./lint.js";
export { proxyCommand } from "./proxy.js";
export { runCommand } from "./run.js";
export { testCommand } from "./test.js";
export { visualiseCommand } from "./visualise.js";
189 changes: 189 additions & 0 deletions packages/cli/src/commands/visualise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { existsSync, writeFileSync } from "fs";
import {
AppError,
dirnameForModule,
environment,
isNil,
pathJoin,
spawn,
uuid,
} from "@lbu/stdlib";
import { formatGraphOfSql } from "../visualise/sql.js";

const SUB_COMMANDS = ["sql", "router"];
const codeGenImportPath = pathJoin(
dirnameForModule(import.meta),
"../../../code-gen/src/internal-exports.js",
);

/**
* Execute the visualise command
*
* @param {Logger} logger
* @param {UtilCommand} command
* @returns {Promise<{ exitCode?: number }>}
*/
export async function visualiseCommand(logger, command) {
const [subCommand, structureFile] = command.arguments;

// All pre-checks

if (isNil(subCommand) || isNil(structureFile)) {
logger.error(
`Usage: lbu visualise [sql,router] {path/to/generated/index.js}`,
);
return { exitCode: 1 };
}

const resolvedStructureFile = pathJoin(process.cwd(), structureFile);

if (SUB_COMMANDS.indexOf(subCommand) === -1) {
logger.info(
`Unknown command: 'lbu visualise ${
subCommand ?? ""
}'. Please use one of '${SUB_COMMANDS.join("', '")}'`,
);
return { exitCode: 1 };
}

const codeGen = await getCodeGenExports();
if (!codeGen) {
logger.error(`The visualiser needs @lbu/code-gen to run.`);
return { exitCode: 1 };
}

if (!(await structureFileExists(resolvedStructureFile))) {
logger.error(
`The specified path '${structureFile}' is not available, or can not be imported. Make sure it exists and is a JavaScript file.`,
);
return { exitCode: 1 };
}

// Get the structure

const { structure, trie } = await getStructure(
logger,
codeGen,
subCommand,
resolvedStructureFile,
);

if (!structure) {
logger.error(
`The structure file could not be loaded. Please ensure that 'dumpStructure' options is enabled while generating.`,
);
return { exitCode: 1 };
}

// Execute and write

let graph;
if (subCommand === "sql") {
graph = formatGraphOfSql(codeGen, structure);
} else if (subCommand === "router") {
logger.info(
`Not implemented. ${
trie ? "Trie exists" : "Trie does not exist either."
}.`,
);
}

const tmpPathDot = `/tmp/${uuid()}.gv`;
const tmpOutputPath = `/tmp/${environment.APP_NAME.toLowerCase()}_${subCommand}.svg`;

writeFileSync(tmpPathDot, graph, "utf8");

logger.info(`Dot file written to temporary directory. Spawning 'dot'.`);
const { exitCode } = await spawn(`dot`, [
"-Tsvg",
`-o`,
tmpOutputPath,
tmpPathDot,
]);

if (exitCode !== 0) {
logger.error(
"'Dot' returned with an error. Please check the above output.",
);
}

logger.info(`Image of '${subCommand}' is available at ${tmpOutputPath}`);
}

/**
* Get the structure using @lbu/code-gen internal functions. This ensures all references
* are linked and the structure is valid.
*
* @param {Logger} logger
* @param codeGen
* @param {"router"|"sql"} subCommand
* @param {string} structureFile
* @returns {Promise<{trie, structure: CodeGenStructure}|undefined>}
*/
async function getStructure(logger, codeGen, subCommand, structureFile) {
const { structure } = await import(structureFile);

let trie;
const context = {
structure,
errors: [],
};

try {
codeGen.linkupReferencesInStructure(context);
codeGen.addFieldsOfRelations(context);

if (subCommand === "sql") {
codeGen.doSqlChecks(context);
}
if (subCommand === "router") {
trie = codeGen.buildTrie(context.structure);
}

codeGen.exitOnErrorsOrReturn(context);

return {
structure: context.structure,
trie,
};
} catch (e) {
if (AppError.instanceOf(e)) {
logger.error(AppError.format(e));
} else if (e.message) {
logger.error(e);
}
return undefined;
}
}

/**
* Check if the code-gen 'internal-exports' file can be imported and import it
*/
async function getCodeGenExports() {
if (!existsSync(codeGenImportPath)) {
return undefined;
}

try {
return await import(codeGenImportPath);
} catch {
return undefined;
}
}

/**
* Check if the passed in structure file exists
*/
async function structureFileExists(structureFile) {
if (!existsSync(structureFile)) {
return false;
}

try {
const imported = await import(structureFile);

return !isNil(imported?.structure);
} catch {
return false;
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
proxyCommand,
runCommand,
testCommand,
visualiseCommand,
} from "./commands/index.js";

const utilCommands = {
help: helpCommand,
init: initCommand,
docker: dockerCommand,
proxy: proxyCommand,
visualise: visualiseCommand,
};

const execCommands = {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { existsSync } from "fs";
* List of commands that don't need to parse node args, script args and tooling args
* @type {string[]}
*/
const utilCommands = ["init", "help", "docker", "proxy"];
const utilCommands = ["init", "help", "docker", "proxy", "visualise"];

/**
* Object of commands that accept special input like node arguments, script name or
Expand Down
98 changes: 98 additions & 0 deletions packages/cli/src/visualise/sql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { environment, isNil } from "@lbu/stdlib";

/**
* Format an ERD like structure with graphviz.
* For html formatting or other documentation see
* https://graphviz.org/doc/info/shapes.html#html
*
* @param codeGen
* @param {CodeGenStructure} structure
* @returns {string}
*/
export function formatGraphOfSql(codeGen, structure) {
const entities = codeGen.getQueryEnabledObjects({ structure });

let src = `
digraph ${environment.APP_NAME} {
label = "Entity Relation Diagram generated by LBU";
fontsize = 14;
rankdir = "TB";
nodesep = 0.7;
`;

for (const entity of entities) {
src += `
${entity.name} [shape=none, margin=0, label=<
${formatEntityTable(codeGen, entity)}
> ];\n`;
}

for (const entity of entities) {
const entityKeys = codeGen.getSortedKeysForType(entity);

for (const relation of entity.relations) {
if (["oneToOneReverse", "oneToMany"].indexOf(relation.subType) !== -1) {
continue;
}

const label = relation.subType === "oneToOne" ? "1 - 1" : "N - 1";

if (relation.reference.reference === entity) {
src += `${entity.name}:"${entityKeys.indexOf(relation.ownKey)}" -> ${
entity.name
}:"${0}" [label="${label}"];\n`;
} else {
src += `${entity.name}:"${entityKeys.indexOf(
relation.ownKey,
)}_right" -> ${
relation.reference.reference.name
}:"${0}" [label="${label}"];\n`;
}
}
}

src += "}";

return src;
}

/**
* Format entity as table.
* Add some background coloring for primary key & date fields
*
* @param codeGen
* @param {CodeGenObjectType} entity
* @returns {string}
*/
function formatEntityTable(codeGen, entity) {
const keys = codeGen.getSortedKeysForType(entity);
const colorForKey = (key) =>
key !== codeGen.getPrimaryKeyWithType(entity).key
? ["createdAt", "updatedAt", "deletedAt"].indexOf(key) !== -1
? 96
: 92
: 70;
const formatType = (key) => {
const type = entity.keys[key].reference ?? entity.keys[key];
const nullable = type.isOptional && isNil(type.defaultValue);

return `${nullable ? "?" : ""}${type.type}`;
};

return `<table border="0" cellspacing="0" cellborder="1">
<tr><td bgcolor="lightblue2" colspan="2"><font face="Times-bold" point-size="16"> ${
entity.name
} </font></td></tr>
${keys
.map((key, idx) => {
const result = `<tr><td bgcolor="grey${colorForKey(
key,
)}" align="left" port="${idx}"><font face="Times-bold"> ${key} </font></td><td align="left" port="${idx}_right"><font color="#535353"> ${formatType(
key,
)} </font></td></tr>`;

return result;
})
.join("\n")}
</table>`;
}
1 change: 1 addition & 0 deletions packages/code-gen/src/generator/sql/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function getPrimaryKeyWithType(type) {
* - Nullable fields
* - createdAt, updatedAt, deletedAt
* @param {CodeGenObjectType} type
* @returns {string[]}
*/
export function getSortedKeysForType(type) {
const typeOrder = {
Expand Down
15 changes: 15 additions & 0 deletions packages/code-gen/src/internal-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// This file is a bit of a hack.
// This allows external tooling at some point to use the structure in the same way as the
// code-generator does. For now we keep this a secret, ssssh, but it is already used in
// @lbu/cli for the visualisation tools.

export { exitOnErrorsOrReturn } from "./generator/errors.js";
export { linkupReferencesInStructure } from "./generator/linkup-references.js";
export { buildTrie } from "./generator/router/trie.js";
export { addFieldsOfRelations } from "./generator/sql/add-fields.js";
export {
getQueryEnabledObjects,
doSqlChecks,
getSortedKeysForType,
getPrimaryKeyWithType,
} from "./generator/sql/utils.js";