From 33a94ac3f54d552e3f071043625633123346f927 Mon Sep 17 00:00:00 2001 From: Dmytro Nechai Date: Mon, 17 May 2021 14:13:18 +0300 Subject: [PATCH 1/4] Add initial implementation --- lib/mermaid.js | 33 +++++++ lib/plantUML.js | 43 ++++++++ metagram.js | 93 +++++++++++++++++- package-lock.json | 243 +++++++++++++++++++++++++++++++++++++++++----- package.json | 8 ++ 5 files changed, 394 insertions(+), 26 deletions(-) create mode 100644 lib/mermaid.js create mode 100644 lib/plantUML.js diff --git a/lib/mermaid.js b/lib/mermaid.js new file mode 100644 index 0000000..efe769c --- /dev/null +++ b/lib/mermaid.js @@ -0,0 +1,33 @@ +'use strict'; + +const serializeProp = (prop) => ` ${prop.type} ${prop.name}`; + +const serializeEntities = (entityInfos) => + entityInfos + .map( + ({ name, props }) => + `${name} {\n` + props.map(serializeProp).join('\n') + '\n}\n' + ) + .join('\n'); + +const serializeRelation = (relation) => { + const sourceConnector = relation.unique ? '|o' : '}o'; + const targetConnector = + (relation.required ? '|' : 'o') + (relation.many ? '{' : '|'); + return ( + `${relation.source} ${sourceConnector}-` + + `-${targetConnector} ${relation.target} : ${relation.name}` + ); +}; + +const serializeRelations = (entityInfos) => + entityInfos + .filter(({ relations }) => relations.length !== 0) + .map(({ relations }) => relations.map(serializeRelation).join('\n')) + .join('\n\n'); + +const serializeERD = (entityInfos) => `erDiagram +${serializeEntities(entityInfos)} +${serializeRelations(entityInfos)}`; + +module.exports = serializeERD; diff --git a/lib/plantUML.js b/lib/plantUML.js new file mode 100644 index 0000000..6c2c0c0 --- /dev/null +++ b/lib/plantUML.js @@ -0,0 +1,43 @@ +'use strict'; + +const serializeProp = (prop) => + ` ${prop.required ? '*' : ''}${prop.name}: ${prop.type}`; + +const serializeEntities = (entityInfos) => + entityInfos + .map( + ({ name, props }) => + `entity ${name} {\n` + props.map(serializeProp).join('\n') + '\n}\n' + ) + .join('\n'); + +const serializeRelation = (relation) => { + const sourceConnector = relation.unique ? '|o' : '}o'; + const targetConnector = + (relation.required ? '|' : 'o') + (relation.many ? '{' : '|'); + return ( + `${relation.source} ${sourceConnector}-` + + `-${targetConnector} ${relation.target} : ${relation.name}` + ); +}; + +const serializeRelations = (entityInfos) => + entityInfos + .filter(({ relations }) => relations.length !== 0) + .map(({ relations }) => relations.map(serializeRelation).join('\n')) + .join('\n\n'); + +const serializeERD = (entityInfos) => `@startuml + +' hide the spot +hide circle + +' avoid problems with angled crows feet +skinparam linetype ortho + +${serializeEntities(entityInfos)} +${serializeRelations(entityInfos)} + +@enduml`; + +module.exports = serializeERD; diff --git a/metagram.js b/metagram.js index f5f048b..0c0d630 100644 --- a/metagram.js +++ b/metagram.js @@ -1,3 +1,94 @@ +#!/usr/bin/env node + 'use strict'; -/* This is a stub */ +const fs = require('fs'); + +const metaschema = require('metaschema'); +const { toUpperCamel } = require('metautil'); + +const yargs = require('yargs'); + +const plantUML = require('./lib/plantUML.js'); +const mermaid = require('./lib/mermaid.js'); + +const SERIALIZERS = { plantUML, mermaid }; + +const args = yargs + .option('path', { + alias: 'p', + type: 'string', + demandOption: true, + describe: 'Path to the directory that contains schemas', + }) + .option('type', { + alias: 't', + type: 'string', + default: 'plantUML', + choices: ['plantUML', 'mermaid'], + }) + .option('output', { + alias: 'o', + type: 'string', + describe: + 'Path to the output file. ' + + 'If not provided, output will be printed to stdout', + }) + .help().argv; + +run(args).catch((error) => console.error(error)); + +async function run({ path, type, output }) { + const model = await metaschema.Model.load(path); + const entityInfos = [...model.entities].map(([name, schema]) => ({ + name, + ...getPropsAndRelations(model, schema), + })); + + const erd = SERIALIZERS[type](entityInfos); + print(output, erd); +} + +function getPropsAndRelations(model, schema, path = '') { + /* { name, type, required } */ + const props = []; + /* { name: source, target, unique, required, many } */ + const relations = []; + + for (const [field, definition] of Object.entries(schema.fields)) { + const name = path ? path + toUpperCamel(field) : field; + if (definition instanceof metaschema.Schema) { + const fieldInfo = getPropsAndRelations(model, definition, name); + props.push(...fieldInfo.props); + relations.push(...fieldInfo.relations); + } else if (model.entities.has(definition.type)) { + props.push({ + name, + type: definition.many ? 'Id[]' : 'Id', + required: definition.required, + }); + /* It's possible that we should check whether target is a detaol*/ + relations.push({ + name, + source: schema.name, + target: definition.type, + unique: definition.unique, + required: definition.required, + many: definition.many, + }); + } else { + props.push({ + name, + type: definition.type, + required: definition.required, + }); + } + } + + return { props, relations }; +} + +function print(path, erd) { + if (!path) return void console.log(erd); + return fs.writeFileSync(path, erd); +} diff --git a/package-lock.json b/package-lock.json index 5eac37b..f09c6d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,11 @@ "name": "@metarhia/metagram", "version": "1.0.0", "license": "MIT", + "dependencies": { + "metaschema": "^1.2.2", + "metautil": "^3.5.3", + "yargs": "^17.0.1" + }, "devDependencies": { "eslint": "^7.26.0", "eslint-config-metarhia": "^7.0.1", @@ -204,7 +209,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true, "engines": { "node": ">=8" } @@ -213,7 +217,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -332,11 +335,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -347,8 +359,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -433,8 +444,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enquirer": { "version": "2.3.6", @@ -504,6 +514,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -914,6 +932,14 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1158,7 +1184,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -1368,6 +1393,46 @@ "node": ">=10" } }, + "node_modules/metaschema": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/metaschema/-/metaschema-1.2.2.tgz", + "integrity": "sha512-61jpPrr74YOPyLZf7cyagwiz9uXFAmNWHGMKB9i/N8BG8cfh1yl0C99//2nw4KbDRp2ft2BQsHLyKCMLFpUkag==", + "dependencies": { + "metautil": "^3.5.3", + "metavm": "^1.0.2" + }, + "engines": { + "node": "^12.9 || 14 || 15" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/tshemsedinov" + } + }, + "node_modules/metautil": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/metautil/-/metautil-3.5.3.tgz", + "integrity": "sha512-9X7fm59GEr7QmTjPc53B7+HOd4XmCL6efxPUI0argyy888LEhxX/XVS1jNEIm6A96qk10mDnPD+gJ2ZMZuTFuQ==", + "engines": { + "node": "^12.9 || 14 || 15" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/tshemsedinov" + } + }, + "node_modules/metavm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/metavm/-/metavm-1.0.2.tgz", + "integrity": "sha512-QiEBgamuEzimKR+o+xgeo/+sgXKT4uXsWIt5OBHSv1RVkSTqebvIKYaj/Ta+4M2A5efAPbvmlv8O/MI49vewoA==", + "engines": { + "node": "^12.9 || 14 || 15" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/tshemsedinov" + } + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1743,6 +1808,14 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,7 +1957,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1924,7 +1996,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.0" }, @@ -2126,17 +2197,66 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true + }, + "node_modules/yargs": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "engines": { + "node": ">=10" + } } }, "dependencies": { @@ -2268,7 +2388,8 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "dev": true, + "requires": {} }, "ajv": { "version": "6.12.6", @@ -2291,14 +2412,12 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -2384,11 +2503,20 @@ "supports-color": "^7.1.0" } }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -2396,8 +2524,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "concat-map": { "version": "0.0.1", @@ -2462,8 +2589,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "enquirer": { "version": "2.3.6", @@ -2518,6 +2644,11 @@ "is-symbol": "^1.0.2" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2573,13 +2704,15 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-metarhia/-/eslint-config-metarhia-7.0.1.tgz", "integrity": "sha512-a5Fstv58FS8U8JubOzmNeSGVvJul5U8OLPlhvHomRroJmL1Eq4gObTvpbnOD9mOs2iRVK7sdwZEkmYbDCp7RlA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-config-prettier": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.4", @@ -2853,6 +2986,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -3024,8 +3162,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { "version": "4.0.1", @@ -3181,6 +3318,25 @@ "yallist": "^4.0.0" } }, + "metaschema": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/metaschema/-/metaschema-1.2.2.tgz", + "integrity": "sha512-61jpPrr74YOPyLZf7cyagwiz9uXFAmNWHGMKB9i/N8BG8cfh1yl0C99//2nw4KbDRp2ft2BQsHLyKCMLFpUkag==", + "requires": { + "metautil": "^3.5.3", + "metavm": "^1.0.2" + } + }, + "metautil": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/metautil/-/metautil-3.5.3.tgz", + "integrity": "sha512-9X7fm59GEr7QmTjPc53B7+HOd4XmCL6efxPUI0argyy888LEhxX/XVS1jNEIm6A96qk10mDnPD+gJ2ZMZuTFuQ==" + }, + "metavm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/metavm/-/metavm-1.0.2.tgz", + "integrity": "sha512-QiEBgamuEzimKR+o+xgeo/+sgXKT4uXsWIt5OBHSv1RVkSTqebvIKYaj/Ta+4M2A5efAPbvmlv8O/MI49vewoA==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -3462,6 +3618,11 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3570,7 +3731,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3601,7 +3761,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, "requires": { "ansi-regex": "^5.0.0" } @@ -3759,17 +3918,51 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true + }, + "yargs": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" } } } diff --git a/package.json b/package.json index 18ec679..8372f12 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "ERD generator for metaschema", "main": "metagram.js", + "bin": { + "metagram": "metagram.js" + }, "files": [ "lib/" ], @@ -22,6 +25,11 @@ "url": "https://github.com/metarhia/metagram/issues" }, "homepage": "https://github.com/metarhia/metagram#readme", + "dependencies": { + "metaschema": "^1.2.2", + "metautil": "^3.5.3", + "yargs": "^17.0.1" + }, "devDependencies": { "eslint": "^7.26.0", "eslint-config-metarhia": "^7.0.1", From aba1ecd6af84408c3877de96f7305aa4ed69c145 Mon Sep 17 00:00:00 2001 From: Dmytro Nechai Date: Mon, 17 May 2021 17:52:25 +0300 Subject: [PATCH 2/4] Fix many-to-many processing --- metagram.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/metagram.js b/metagram.js index 0c0d630..98ebf33 100644 --- a/metagram.js +++ b/metagram.js @@ -64,10 +64,10 @@ function getPropsAndRelations(model, schema, path = '') { } else if (model.entities.has(definition.type)) { props.push({ name, - type: definition.many ? 'Id[]' : 'Id', + type: 'Id', required: definition.required, }); - /* It's possible that we should check whether target is a detaol*/ + /* It's possible that we should check whether target is a detail*/ relations.push({ name, source: schema.name, @@ -85,6 +85,16 @@ function getPropsAndRelations(model, schema, path = '') { } } + for (const [name, index] of Object.entries(schema.indexes)) { + if (!index.many) continue; + relations.push({ + name, + source: schema.name, + target: index.many, + many: true, + }); + } + return { props, relations }; } From 18fe8ff16de5a8de8f4887e31a7c9b05297e479e Mon Sep 17 00:00:00 2001 From: Dmytro Nechai Date: Tue, 18 May 2021 11:00:39 +0300 Subject: [PATCH 3/4] Show metaschema warnings --- metagram.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/metagram.js b/metagram.js index 98ebf33..65467b4 100644 --- a/metagram.js +++ b/metagram.js @@ -34,12 +34,20 @@ const args = yargs 'Path to the output file. ' + 'If not provided, output will be printed to stdout', }) + .option('warn', { + alias: 'w', + type: 'boolean', + default: true, + describe: 'Print metaschema warnings', + }) .help().argv; run(args).catch((error) => console.error(error)); -async function run({ path, type, output }) { +async function run({ path, type, output, warn }) { const model = await metaschema.Model.load(path); + if (warn && model.warnings.length !== 0) + model.warnings.forEach((warning) => console.warn(warning)); const entityInfos = [...model.entities].map(([name, schema]) => ({ name, ...getPropsAndRelations(model, schema), From 0ccc562c484f6c99fe74481242051f95ca117392 Mon Sep 17 00:00:00 2001 From: Dmytro Nechai Date: Tue, 18 May 2021 18:05:31 +0300 Subject: [PATCH 4/4] Add possibility to ommit relation names --- lib/plantUML.js | 22 ++++++++++++++-------- metagram.js | 11 +++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/plantUML.js b/lib/plantUML.js index 6c2c0c0..e0ef379 100644 --- a/lib/plantUML.js +++ b/lib/plantUML.js @@ -11,23 +11,29 @@ const serializeEntities = (entityInfos) => ) .join('\n'); -const serializeRelation = (relation) => { +const serializeRelation = (relation, config) => { const sourceConnector = relation.unique ? '|o' : '}o'; const targetConnector = (relation.required ? '|' : 'o') + (relation.many ? '{' : '|'); - return ( + const property = `${relation.source} ${sourceConnector}-` + - `-${targetConnector} ${relation.target} : ${relation.name}` - ); + `-${targetConnector} ${relation.target}`; + return config.annotatedRelations + ? property + ` : ${relation.name}` + : property; }; -const serializeRelations = (entityInfos) => +const serializeRelations = (entityInfos, config) => entityInfos .filter(({ relations }) => relations.length !== 0) - .map(({ relations }) => relations.map(serializeRelation).join('\n')) + .map(({ relations }) => + relations + .map((relation) => serializeRelation(relation, config)) + .join('\n') + ) .join('\n\n'); -const serializeERD = (entityInfos) => `@startuml +const serializeERD = (entityInfos, config) => `@startuml ' hide the spot hide circle @@ -36,7 +42,7 @@ hide circle skinparam linetype ortho ${serializeEntities(entityInfos)} -${serializeRelations(entityInfos)} +${serializeRelations(entityInfos, config)} @enduml`; diff --git a/metagram.js b/metagram.js index 65467b4..24e9cf8 100644 --- a/metagram.js +++ b/metagram.js @@ -40,11 +40,18 @@ const args = yargs default: true, describe: 'Print metaschema warnings', }) + .option('annotatedRelations', { + type: 'boolean', + default: true, + describe: + 'Annotate relation with a name of a property ' + + 'that represents the relation. Only applies to PlantUML', + }) .help().argv; run(args).catch((error) => console.error(error)); -async function run({ path, type, output, warn }) { +async function run({ path, type, output, warn, annotatedRelations }) { const model = await metaschema.Model.load(path); if (warn && model.warnings.length !== 0) model.warnings.forEach((warning) => console.warn(warning)); @@ -53,7 +60,7 @@ async function run({ path, type, output, warn }) { ...getPropsAndRelations(model, schema), })); - const erd = SERIALIZERS[type](entityInfos); + const erd = SERIALIZERS[type](entityInfos, { annotatedRelations }); print(output, erd); }