= ({
17 | label,
18 | onClick,
19 | type = ButtonTypes.BUTTON,
20 | children,
21 | }) => {
22 | return (
23 |
26 | );
27 | };
28 |
29 | export default Button;
30 |
--------------------------------------------------------------------------------
/app/angular/components/conversionAttributeModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./conversionAttributeModal.html";
3 |
4 | const app = angular.module("app.createModelModal", []);
5 |
6 | const Controller = function () {
7 | const $ctrl = this;
8 | $ctrl.selectedValue = "new_column"
9 |
10 | $ctrl.forward = (selectedOption) => {
11 | $ctrl.submitted = true;
12 | $ctrl.close({ result: {"value": selectedOption, "quantity": $ctrl.qt}});
13 | };
14 | };
15 |
16 | export default app.component("conversionAttributeModal", {
17 | template: template,
18 | bindings: {
19 | options: "<",
20 | summary: "<",
21 | title: "<",
22 | close: "&",
23 | },
24 | controller: Controller,
25 | }).name;
26 |
--------------------------------------------------------------------------------
/app/angular/service/queryExpressionService.js:
--------------------------------------------------------------------------------
1 | export const comparasionOperators = {
2 | EQUAL_TO: (value) => `= ${value}`,
3 | NOT_EQUAL_TO: (value) => `<> ${value}`,
4 | GREATER_THAN: (value) => `> ${value}`,
5 | LESS_THAN: (value) => `< ${value}`,
6 | BETWEEN: (value, otherValue) => `BETWEEN ${value} AND ${otherValue}`,
7 | GREATER_THAN_OR_EQUAL_TO: (value) => `>= ${value}`,
8 | LESS_THAN_OR_EQUAL_TO: (value) => `<= ${value}`,
9 | IS: (value) => `= '${value}'`,
10 | IS_NOT: (value) => `<> '${value}'`,
11 | CONTAINS: (value) => `LIKE '%${value}%'`,
12 | DOES_NOT_CONTAIN: (value) => `NOT LIKE '%${value}%'`,
13 | STARTS_WITH: (value) => `LIKE '${value}%'`,
14 | ENDS_WITH: (value) => `LIKE '%${value}'`,
15 | }
--------------------------------------------------------------------------------
/app/angular/components/icons/logic/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | mongo:
4 | container_name: mongo
5 | image: mongo
6 | volumes:
7 | - ./data:/data/db
8 | ports:
9 | - "27017:27017"
10 | app_server:
11 | container_name: app_server
12 | build: .
13 | command: yarn start:dev
14 | volumes:
15 | - .:/usr/src/app
16 | - /usr/src/app/node_modules
17 | ports:
18 | - "3000:3000"
19 | depends_on:
20 | - mongo
21 | app_frontend:
22 | container_name: app_frontend
23 | build: .
24 | command: yarn start:frontend
25 | volumes:
26 | - .:/usr/src/app
27 | - /usr/src/app/node_modules
28 | ports:
29 | - "9000:9000"
30 | depends_on:
31 | - app_server
32 |
--------------------------------------------------------------------------------
/app/sass/modelsList.scss:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////
2 | // .modelsList header.container
3 | // Adjustments on Boostrap to accommodate .feedback block
4 | ///////////////////////////////////////////////////////////////////////////////
5 | .modelsList header.container {
6 | position: relative;
7 | }
8 |
9 | .modelsList header.container .page-header {
10 | margin-top: 60px
11 | }
12 |
13 | ///////////////////////////////////////////////////////////////////////////////
14 | // .modelsList .feedback
15 | ///////////////////////////////////////////////////////////////////////////////
16 | .modelsList .feedback {
17 | position: absolute;
18 | top: 15px;
19 | right: 15px;
20 | left: 15px;
21 |
22 | }
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | //"@babel/env",
4 | "@babel/react",
5 | [
6 | "@babel/preset-env",
7 | {
8 | "modules": false
9 | },
10 | "jest"
11 | ]
12 | ],
13 | "plugins": [
14 | "@babel/plugin-proposal-class-properties",
15 | "@babel/plugin-syntax-dynamic-import",
16 | "angularjs-annotate",
17 | ["@babel/plugin-transform-runtime",
18 | {
19 | "regenerator": true
20 | }
21 | ]
22 | ],
23 | "env": {
24 | "test": {
25 | "presets": [
26 | [
27 | "@babel/preset-env",
28 | {
29 | "modules": "auto"
30 | },
31 | "jest"
32 | ]
33 | ],
34 | "plugins": ["@babel/plugin-transform-runtime"]
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/angular/components/conversionOptionModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
{{$ctrl.summary}}
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/sass/atomic.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Hide Stuff
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .hide {display: none !important;}
5 |
6 | .vhide {
7 | position: absolute !important;
8 | width: 1px !important;
9 | height: 1px !important;
10 | border: 0 !important;
11 | padding: 0 !important;
12 | margin: -1px !important;
13 | overflow: hidden !important;
14 | clip: rect(0 0 0 0) !important;
15 | }
16 |
17 | ////////////////////////////////////////////////////////////////////////////////
18 | // .separator
19 | ////////////////////////////////////////////////////////////////////////////////
20 | .separator {
21 | width: 100%;
22 | border-color: #cecece;
23 | }
--------------------------------------------------------------------------------
/app/angular/components/renameModelModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./renameModelModal.html";
3 |
4 | const app = angular.module("app.renameModelModal", []);
5 |
6 | const Controller = function () {
7 | const $ctrl = this;
8 | $ctrl.submitted = false;
9 |
10 | $ctrl.rename = (newName) => {
11 | $ctrl.submitted = true;
12 | if (newName != null && newName != "") {
13 | $ctrl.close({
14 | result: newName,
15 | });
16 | }
17 | };
18 |
19 | $ctrl.cancel = () => {
20 | $ctrl.dismiss({
21 | reason: "cancel",
22 | });
23 | };
24 | };
25 |
26 | export default app.component("renameModelModal", {
27 | template: template,
28 | bindings: {
29 | close: "&",
30 | dismiss: "&",
31 | },
32 | controller: Controller,
33 | }).name;
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/requisitar-funcionalidade.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Requisitar funcionalidade
3 | about: Ideias e sugestões de como melhorar o BRMW
4 | title: ''
5 | labels: Feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Sua sugestão de melhoria é relacionada a algum problema?
11 |
12 | Adicione aqui a descrição do problema
13 | _Ex: É muito complicado fazer..._
14 |
15 | ---
16 |
17 | ### Descreva sua ideia de como resolver o problema
18 | Adicione aqui como você acha que deveríamos resolver esse problema.
19 | Fique a vontade para adicionar imagens, videos, desenhos ou qualquer material que te ajude a explicar sua proposta de solução
20 |
21 | ---
22 |
23 | ### Informações extras
24 | Aqui você pode adicionar qualquer informação extra que ache relevante pra nos ajudar a entender o problema.
25 |
--------------------------------------------------------------------------------
/app/angular/publicview/publicView.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .publicView
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .publicView > .navbar {
5 | height: 55px;
6 | background-color: #fff;
7 | }
8 |
9 | .publicView .navbar .br-loader {
10 | top: 63px;
11 | }
12 |
13 | .publicView .document-info {
14 | margin-left: 150px;
15 | }
16 |
17 | .publicView .document-info h2 {
18 | margin-top: 5px;
19 | margin-bottom: 15px;
20 | }
21 |
22 | .publicView .model-area {
23 | left: 0;
24 | }
25 |
26 | .publicView .workspace-header .nav li:first-child {
27 | margin-left: 149px;
28 | border-left: 1px solid var(--border-default);
29 | }
30 |
31 | .publicView .workspace-header .header-actions {
32 | position: relative;
33 | top: -3px;
34 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | require("dotenv").config();
3 | const app = require("./server_app/app");
4 | const config = require("./server_app/helpers/config");
5 | const { Port, MongoUrl, ApiUrl, IsDevelopment } = config;
6 |
7 | mongoose.connect(
8 | MongoUrl,
9 | { useNewUrlParser: true, useUnifiedTopology: true },
10 | function (err) {
11 | if (err) throw err;
12 |
13 | const databaseInfo = IsDevelopment ? `Database: ${MongoUrl}\n` : "";
14 |
15 | app.listen(Port, function () {
16 | console.log(`
17 | ---------------------------------------------------
18 | --------------- APPLICATION RUNNING ---------------
19 | ---------------------------------------------------
20 |
21 | App: ${ApiUrl}
22 | ${databaseInfo}
23 | ---------------------------------------------------
24 | `);
25 | });
26 | }
27 | );
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: brmodeloweb # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/app/angular/components/queryExpressionModal/styles.scss:
--------------------------------------------------------------------------------
1 | .query-expression-modal {
2 | .labels-wrapper {
3 | margin-bottom: 12px;
4 | display: flex;
5 | flex-wrap: wrap;
6 | row-gap: 18px;
7 |
8 | .condition-label {
9 | padding: 6px;
10 | background: hsl(170, 100%, 20%);
11 | color: white;
12 | border-radius: 4px;
13 | margin-right: 8px;
14 |
15 | i {
16 | margin-left: 4px;
17 | cursor: pointer;
18 | }
19 | }
20 | }
21 | .form-content {
22 | display: flex;
23 | align-items: center;
24 | column-gap: 8px;
25 | margin-bottom: 12px;
26 |
27 | button {
28 | min-width: 180px;
29 | }
30 |
31 | .dropdown-tmp-list {
32 | max-height: 400px;
33 | overflow: auto;
34 | }
35 | }
36 | .form-content-joins {
37 | dropdown {
38 | width: 100%;
39 | }
40 | }
41 | .modal-message {
42 | margin-top: 16px;
43 | }
44 | }
--------------------------------------------------------------------------------
/app/sass/print.scss:
--------------------------------------------------------------------------------
1 | @media print {
2 |
3 | ////////////////////////////////////////////////////////////////////////////
4 | // Hide not relevant interface when printing
5 | ////////////////////////////////////////////////////////////////////////////
6 | .elements-list,
7 | .main-header,
8 | .elements-holder,
9 | .workspace-header,
10 | .model-properties {
11 | display: none;
12 | }
13 |
14 | ////////////////////////////////////////////////////////////////////////////
15 | // Prepare printing area
16 | ////////////////////////////////////////////////////////////////////////////
17 | .model-area {
18 | position: static;
19 | }
20 |
21 | .mainContent,
22 | .editor-scroller {
23 | padding: 0 !important;
24 | margin: 0 !important;
25 | }
26 |
27 | .editor-scroller .joint-paper {
28 | position: static;
29 | height: 100vh;
30 | border-color: transparent;
31 | box-shadow: 0 0 0 transparent;
32 | }
33 | }
--------------------------------------------------------------------------------
/app/angular/components/dropdown.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./dropdown.html";
3 |
4 | const dropDownController = function ($element, $timeout) {
5 | const ctrl = this;
6 | ctrl.open = false;
7 |
8 | ctrl.toggle = (command) => {
9 | ctrl.open = command;
10 | };
11 |
12 | ctrl.select = (option) => {
13 | ctrl.selected = option;
14 | ctrl.toggle(false);
15 | ctrl.onSelect({ selected: option });
16 | };
17 |
18 | ctrl.$postLink = () => {
19 | $element.on("focusout", function () {
20 | if (ctrl.open) {
21 | $timeout(function () {
22 | ctrl.toggle(false);
23 | }, 300);
24 | }
25 | });
26 | };
27 |
28 | };
29 |
30 | export default angular.module("app.dropdown", []).component("dropdown", {
31 | template,
32 | controller: dropDownController,
33 | bindings: {
34 | options: "<",
35 | selected: "<",
36 | onSelect: "&",
37 | disabled: '<'
38 | },
39 | }).name;
40 |
--------------------------------------------------------------------------------
/app/angular/components/supportBannersList/index.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // Component base
3 | ////////////////////////////////////////////////////////////////////////////////
4 | support-banners-list {
5 | display: block;
6 | }
7 |
8 | .support-banner-list {
9 | list-style: none;
10 | padding: 0;
11 | }
12 |
13 | ////////////////////////////////////////////////////////////////////////////////
14 | // .small-banner
15 | ////////////////////////////////////////////////////////////////////////////////
16 | .small-banner a {
17 | display: block;
18 | border: 1px solid var(--gray-80);
19 | padding: 8px 6px;
20 | border-radius: 4px;
21 | margin-bottom: 12px;
22 | color: var(--gray-50);
23 | }
24 |
25 | .small-banner a:hover {
26 | text-decoration: none;
27 | background-color: #fff;
28 | }
29 |
30 | .small-banner p {
31 | margin: 0;
32 | font-size: 1.3rem;
33 | }
--------------------------------------------------------------------------------
/server_app/helpers/crypto.test.js:
--------------------------------------------------------------------------------
1 | const { encrypt, decrypt } = require("./crypto");
2 |
3 | describe("crypto test", () => {
4 | const OLD_ENV = process.env;
5 |
6 | beforeEach(() => {
7 | jest.resetModules();
8 | process.env = { ...OLD_ENV };
9 | });
10 |
11 | afterAll(() => {
12 | process.env = OLD_ENV;
13 | });
14 |
15 | test("Encrypt data", async () => {
16 | process.env.SECRET_TOKEN = "some_token";
17 | const data = { name: "some_name" };
18 | const json = JSON.stringify(data);
19 |
20 | const encrypted = encrypt(json);
21 |
22 | expect(encrypted).not.toBeNull();
23 | });
24 |
25 | test("Decrypt data", async () => {
26 | process.env.SECRET_TOKEN = "some_token";
27 | const data = { name: "some_name" };
28 | const json = JSON.stringify(data);
29 |
30 | const decrypted = decrypt(encrypt(json));
31 |
32 | expect(decrypted).not.toBeNull();
33 | expect(JSON.parse(decrypted)).toEqual(data);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/app/angular/components/dropdownIcon.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./dropdownIcon.html";
3 |
4 | const dropDownIconController = function ($element, $timeout) {
5 | const ctrl = this;
6 | ctrl.open = false;
7 |
8 | ctrl.toggle = (command) => {
9 | ctrl.open = command;
10 | };
11 |
12 | ctrl.select = (option) => {
13 | ctrl.selected = option;
14 | ctrl.toggle(false);
15 | ctrl.onSelect({ selected: option });
16 | };
17 |
18 | ctrl.$postLink = () => {
19 | $element.on("focusout", function () {
20 | if (ctrl.open) {
21 | $timeout(function () {
22 | ctrl.toggle(false);
23 | }, 150);
24 | }
25 | });
26 | };
27 | };
28 |
29 | export default angular.module("app.dropdownIcon", []).component("dropdownIcon", {
30 | template,
31 | controller: dropDownIconController,
32 | bindings: {
33 | options: "<",
34 | selected: "<",
35 | onSelect: "&",
36 | },
37 | transclude: true
38 | }).name;
39 |
--------------------------------------------------------------------------------
/app/angular/logic/checkConstraint.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./checkConstraint.html";
3 |
4 | const app = angular.module("app.checkConstraint", []);
5 |
6 | const Controller = function () {
7 | const $ctrl = this;
8 |
9 | $ctrl.isManual = true;
10 |
11 | $ctrl.toggleFormType = () => {
12 | $ctrl.isManual = !$ctrl.isManual;
13 | }
14 |
15 | $ctrl.selectCheckConstraint = (selected) => {
16 | $ctrl.column.checkConstraint = {
17 | type: selected.type,
18 | name: selected.name,
19 | };
20 | }
21 |
22 | $ctrl.$onChanges = (changes) => {
23 | if (changes.column != null && changes.column.currentValue != null) {
24 | $ctrl.column = changes.column.currentValue;
25 | $ctrl.isManual = ($ctrl.column.checkConstraint || {}).checkExpression;
26 | }
27 | }
28 | }
29 |
30 | export default app.component("checkConstraint", {
31 | template: template,
32 | bindings: {
33 | column: "<",
34 | },
35 | controller: Controller,
36 | }).name;
--------------------------------------------------------------------------------
/app/sass/feedback.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .feedback
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .workspace-header .feedback {
5 | position: absolute;
6 | top: calc(100% + 12px);
7 | left: 12px;
8 | width: calc(100% - 64px);
9 | z-index: 1;
10 | }
11 |
12 | ////////////////////////////////////////////////////////////////////////////////
13 | // .field.error
14 | ////////////////////////////////////////////////////////////////////////////////
15 | .field.error .fieldLabel {
16 | color: var (--danger-base);
17 | }
18 |
19 | .field.error .fieldInput {
20 | color: var(--danger-base);
21 | border-color: var(--danger-base);
22 | // background-color: rgba(red, 0.1);
23 | box-shadow: 1px 1px 1px var(--white) inset;
24 | }
25 |
26 | .field.error .fieldInput:focus {box-shadow: 1px 1px 1px var(--white) inset;}
27 | .field.error .fieldInput::placeholder {color: rgba(red, 0.6);}
--------------------------------------------------------------------------------
/app/angular/components/confirmationModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./confirmationModal.html";
3 |
4 | const app = angular.module("app.confirmationModalController", []);
5 |
6 | function controller() {
7 | const $ctrl = this;
8 |
9 | $ctrl.$onInit = () => {
10 | const {
11 | title,
12 | content,
13 | cancelLabel = "Cancelar",
14 | confirmLabel = "Confirmar",
15 | } = $ctrl.resolve.modalData;
16 |
17 | $ctrl.title = title;
18 | $ctrl.content = content;
19 | $ctrl.cancelLabel = cancelLabel;
20 | $ctrl.confirmLabel = confirmLabel;
21 | };
22 |
23 | $ctrl.confirm = () => {
24 | $ctrl.close({
25 | result: true,
26 | });
27 | };
28 |
29 | $ctrl.cancel = () => {
30 | $ctrl.dismiss({
31 | reason: "cancel",
32 | });
33 | };
34 | }
35 |
36 | export default app.component("confirmationModal", {
37 | template,
38 | bindings: {
39 | close: "&",
40 | dismiss: "&",
41 | resolve: "<",
42 | },
43 | controller,
44 | }).name;
45 |
--------------------------------------------------------------------------------
/app/angular/components/conversionAttributeModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
{{$ctrl.summary}}
7 |
8 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/angular/components/duplicateModelModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/reportar-problema.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Reportar Problema
3 | about: Reportar bugs no BRMW
4 | title: ''
5 | labels: Bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Link da modelagem
11 | Adicione aqui o link da modelagem na qual você estava trabalhando quando encontrou o problema.
12 |
13 | ---
14 |
15 | ### Descreva o problema
16 | Adicione aqui uma breve descrição do problema.
17 |
18 | ---
19 |
20 | ### Passos para reproduzir
21 | 1. Na tela de...
22 | 1. Selecione a opção...
23 | 1. Clique em...
24 |
25 | ---
26 |
27 | ### Screenshots e/ou video
28 | Adicione aqui imagens ou videos que possam nos ajudar a entender o problema
29 |
30 | ---
31 |
32 | ### Sistema operacional e navegador
33 | - Adicione aqui seu sistema operacional (Windows, MacOS, Linux distro, etc...)
34 | - Adicione aqui seu navegador (Firefox, Chrome, Edge, etc) e a versão do mesmo
35 |
36 | ---
37 |
38 | ### Informações extras
39 | Aqui você pode adicionar qualquer informação extra que ache relevante pra nos ajudar a entender o problema.
40 |
--------------------------------------------------------------------------------
/app/sass/modal.scss:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////
2 | // Modal customizations
3 | ///////////////////////////////////////////////////////////////////////////////
4 | .modal-content {
5 | background-color: #efefef;
6 | }
7 |
8 | .modal-title {
9 | color: var(--gray-20);
10 | }
11 |
12 | ///////////////////////////////////////////////////////////////////////////////
13 | // Loading indicator inside a modal
14 | ///////////////////////////////////////////////////////////////////////////////
15 | .modal-header {
16 | position: relative;
17 | }
18 |
19 | .modal-header .br-loader {
20 | position: absolute;
21 | top: unset;
22 | bottom: -9px;
23 | }
24 |
25 | ///////////////////////////////////////////////////////////////////////////////
26 | // For validation inside modal
27 | ///////////////////////////////////////////////////////////////////////////////
28 | .modal-body .feedback .alert {
29 | padding: 6px 10px;
30 | }
31 |
32 | .modal-body .feedback .alert p {
33 | font-size: 1.2rem;
34 | }
--------------------------------------------------------------------------------
/app/angular/components/renameModelModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
23 |
24 |
--------------------------------------------------------------------------------
/app/angular/conceptual/validator.js:
--------------------------------------------------------------------------------
1 | export default class Validator {
2 |
3 | constructor() {}
4 |
5 | isEntity = (element) => {
6 | return element.attributes.supertype === 'Entity';
7 | };
8 |
9 | isAttribute = (element) => {
10 | return element.attributes.supertype === 'Attribute';
11 | };
12 |
13 | isExtension = (element) => {
14 | return element.attributes.supertype === 'Inheritance';
15 | };
16 |
17 | isRelationship = (element) => {
18 | return element.attributes.supertype === 'Relationship'
19 | };
20 |
21 | isAssociative = (element) => {
22 | return element.attributes.type === 'erd.Associative'
23 | };
24 |
25 | isKey = (element) => {
26 | return element.attributes.supertype === 'Key';
27 | };
28 |
29 | isComposedAttribute = (element) => {
30 | return element.attributes.type === 'erd.ComposedAttribute';
31 | };
32 |
33 | isLink = (element) => {
34 | return element.attributes.type === 'erd.Link';
35 | };
36 |
37 | isNote = (element) => {
38 | return element.attributes.type === 'custom.Note';
39 | };
40 |
41 | }
--------------------------------------------------------------------------------
/app/angular/service/toolsViewService.js:
--------------------------------------------------------------------------------
1 | import * as joint from "jointjs/dist/joint";
2 |
3 | export default class ToolsViewService {
4 | constructor() {
5 | const defaultTools = this.#createDefaultTools();
6 | this.toolsWithInfo = new joint.dia.ToolsView({ tools: [...defaultTools, new joint.shapes.erd.InfoButton(), new joint.linkTools.Remove()] });
7 | this.tools = new joint.dia.ToolsView({ tools: [...defaultTools, new joint.linkTools.Remove()] })
8 | this.toolsWithoutRemove = new joint.dia.ToolsView({ tools: defaultTools });
9 | }
10 |
11 | getToolsView(conectionType = "") {
12 | if (conectionType === "Entity-Relationship") return this.toolsWithInfo;
13 | if (conectionType === "Table-View") return this.toolsWithoutRemove;
14 | return this.tools;
15 | }
16 |
17 | #createDefaultTools = () => {
18 | return [
19 | new joint.linkTools.Vertices(),
20 | new joint.linkTools.Segments(),
21 | new joint.linkTools.SourceArrowhead(),
22 | new joint.linkTools.TargetArrowhead(),
23 | new joint.linkTools.Boundary(),
24 | ]
25 | }
26 |
27 | };
--------------------------------------------------------------------------------
/app/angular/components/icons/conceptual/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cypress/e2e/api/model.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | const conceptualModel = require("../../fixtures/conceptualModel.json");
4 |
5 | describe("Models - Creation via API call", () => {
6 | beforeEach(() => {
7 | cy.intercept("GET", "/models?userId=*").as("getUserModels");
8 | cy.loginViaGui();
9 | cy.wait("@getUserModels").then((userModels) => {
10 | cy.cleanUpUserModels(userModels);
11 | });
12 | cy.reload();
13 | });
14 |
15 | it("successfully creates a conceptual model via API", () => {
16 | cy.wait("@getUserModels").then((userModels) => {
17 | const userId = userModels.request.url.match(/userId=([^&]*)/)[1];
18 |
19 | cy.createModelViaApi("conceptual", userId, conceptualModel);
20 | });
21 | cy.reload();
22 | cy.contains("td", "Conceptual").click();
23 |
24 | cy.get(".editor-scroller [data-type='erd.Entity']").should("have.length", 2);
25 | cy.get(".editor-scroller [data-type='erd.Relationship']").should(
26 | "have.length",
27 | 1
28 | );
29 | cy.get(".editor-scroller [data-type='erd.Link']").should("have.length", 2);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/app/react/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import LocaleNamespaces from "@enums/localeNamespaces";
2 | import i18n, { Resource } from "i18next";
3 | import LanguageDetector from "i18next-browser-languagedetector";
4 | import { initReactI18next } from "react-i18next";
5 | import commonTranslationEN from "../public/locales/en/common.json";
6 | import loginTranslationEN from "../public/locales/en/login.json";
7 | import commonTranslationPTBR from "../public/locales/pt-BR/common.json";
8 | import loginTranslationPTBR from "../public/locales/pt-BR/login.json";
9 |
10 | const resources: Resource = {
11 | en: {
12 | common: commonTranslationEN,
13 | login: loginTranslationEN,
14 | },
15 | "pt-BR": {
16 | common: commonTranslationPTBR,
17 | login: loginTranslationPTBR,
18 | },
19 | };
20 |
21 | i18n
22 | .use(LanguageDetector)
23 | .use(initReactI18next)
24 | .init({
25 | resources,
26 | ns: Object.values(LocaleNamespaces),
27 | defaultNS: LocaleNamespaces.COMMON,
28 | debug: process.env.NODE_ENV !== "production",
29 | fallbackLng: "en",
30 | interpolation: {
31 | escapeValue: false,
32 | },
33 | });
34 |
35 | export default i18n;
36 |
--------------------------------------------------------------------------------
/app/sass/table-preview.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .table-preview
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .table-preview {
5 | border: 1px solid var(--border-default);
6 | border-radius: 2px 2px 0 0;
7 | margin-bottom: 12px;
8 | }
9 |
10 | .table-preview header {
11 | padding: 12px 8px;
12 | border-bottom: 1px solid var(--border-default);
13 | font-size: 1.3em;
14 | background-color: rgba(#000, 0.1); // Fallback color
15 | }
16 |
17 | .table-preview header h5 {
18 | font-family: monospace;
19 | font-weight: 500;
20 | margin: 0;
21 | }
22 |
23 | ////////////////////////////////////////////////////////////////////////////////
24 | // .table-preview-body
25 | ////////////////////////////////////////////////////////////////////////////////
26 | .table-preview-body .checkbox {
27 | padding: 6px 8px;
28 | margin: 0;
29 | }
30 |
31 | .table-preview-body .checkbox:not(:last-child) {
32 | border-bottom: 1px solid var(--border-default);
33 | }
34 |
35 | .table-preview-body .checkbox label {
36 | font-size: 0.9em;
37 | }
38 |
--------------------------------------------------------------------------------
/server_app/mail/sender.js:
--------------------------------------------------------------------------------
1 | const formData = require('form-data');
2 | const Mailgun = require('mailgun.js');
3 | const mailgun = new Mailgun(formData);
4 | const templateLoader = require('./templateLoader');
5 |
6 | const getPath = (email, recoveryCode) => {
7 | const baseurl = process.env.PROD_MAIL_ENV || 'http://localhost:3000';
8 | return `${baseurl}/#!/reset/${email}/${recoveryCode}`
9 | }
10 |
11 | const recovery = async (email, recoveryCode) => {
12 | return new Promise(async (resolve, reject) => {
13 | if(!process.env.PROD_MAIL_ENV) {
14 | return resolve(`reset link: ${getPath(email, recoveryCode)}`)
15 | }
16 | const data = {
17 | from: 'brmodeloweb@gmail.com',
18 | to: [email],
19 | subject: 'Recuperação de senha',
20 | html: templateLoader.loadRecovery(getPath(email, recoveryCode)),
21 | };
22 | const mg = mailgun.client({ username: 'api', key: process.env.PROD_MAILGUN_API_KEY });
23 | mg.messages.create('brmodeloweb.com', data)
24 | .then(msg => resolve(msg))
25 | .catch(err => reject(err));
26 | });
27 | };
28 |
29 | const sender = {
30 | recovery
31 | };
32 |
33 | module.exports = sender;
--------------------------------------------------------------------------------
/app/angular/conceptual/factory.js:
--------------------------------------------------------------------------------
1 | export default class Factory {
2 |
3 | constructor(shapes) {
4 | this.shapes = shapes;
5 | }
6 |
7 | createEntity = (customConfig) => {
8 | return new this.shapes.erd.Entity(customConfig);
9 | };
10 |
11 | createRelationship = (customConfig) => {
12 | return new this.shapes.erd.Relationship(customConfig);
13 | };
14 |
15 | createIsa = (customConfig) => {
16 | return new this.shapes.erd.ISA(customConfig);
17 | };
18 |
19 | createAssociative = (customConfig) => {
20 | return new this.shapes.erd.Associative(customConfig)
21 | };
22 |
23 | createAttribute = (customConfig) => {
24 | return new this.shapes.erd.Attribute(customConfig);
25 | };
26 |
27 | createKey = (customConfig) => {
28 | return new this.shapes.erd.Key(customConfig);
29 | };
30 |
31 | createLink = (customConfig) => {
32 | return new this.shapes.erd.Link(customConfig);
33 | };
34 |
35 | createBlockAssociative = (customConfig) => {
36 | return new this.shapes.erd.BlockAssociative(customConfig);
37 | };
38 |
39 | createComposedAttribute = (customConfig) => {
40 | return new this.shapes.erd.ComposedAttribute(customConfig);
41 | };
42 |
43 | }
--------------------------------------------------------------------------------
/app/angular/components/duplicateModelModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./duplicateModelModal.html";
3 |
4 | const app = angular.module("app.duplicateModelModalController", []);
5 |
6 | const Controller = function(ModelAPI) {
7 | const $ctrl = this;
8 | $ctrl.submitted = false;
9 |
10 | $ctrl.$onInit = () => {
11 | $ctrl.name = $ctrl.suggestedName;
12 | };
13 |
14 | $ctrl.save = function (newName) {
15 | $ctrl.submitted = true;
16 | if (newName != null && newName != "") {
17 | ModelAPI.duplicate($ctrl.modelId, $ctrl.userId, newName).then((newModelResponse) => {
18 | $ctrl.close({
19 | result: newModelResponse.data,
20 | });
21 | }).catch(error => {
22 | $ctrl.dismiss({
23 | result: "error",
24 | reason: error
25 | });
26 | });
27 | }
28 | };
29 |
30 | $ctrl.cancel = function () {
31 | $ctrl.dismiss({
32 | reason: "cancel",
33 | });
34 | };
35 | };
36 |
37 | export default app.component("duplicateModelModal", {
38 | template,
39 | bindings: {
40 | close: "&",
41 | dismiss: "&",
42 | suggestedName: "<",
43 | userId: "<",
44 | modelId: "<",
45 | },
46 | controller: Controller,
47 | }).name;
--------------------------------------------------------------------------------
/app/angular/noaccess/noaccess.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server_app/helpers/crypto.js:
--------------------------------------------------------------------------------
1 | const crypto = require("crypto");
2 |
3 | const ALGORITHM = "aes-256-ctr";
4 | const ENCRYPTION_KEY = process.env.SECRET_TOKEN;
5 | const IV_LENGTH = 16;
6 |
7 | const encrypt = (data) => {
8 | const key = crypto
9 | .createHash("sha256")
10 | .update(String(ENCRYPTION_KEY))
11 | .digest("base64")
12 | .substr(0, 32);
13 | const iv = crypto.randomBytes(IV_LENGTH);
14 | const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key), iv);
15 | return Buffer.concat([iv, cipher.update(data), cipher.final()]);
16 | };
17 |
18 | const decrypt = (hash) => {
19 | const key = crypto
20 | .createHash("sha256")
21 | .update(String(ENCRYPTION_KEY))
22 | .digest("base64")
23 | .substr(0, 32);
24 | const iv = hash.slice(0, IV_LENGTH);
25 | const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key), iv);
26 | const data = hash.slice(IV_LENGTH);
27 | const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
28 | return decrypted.toString();
29 | };
30 |
31 | const decode = (hash) => {
32 | return Buffer.from(hash, 'base64').toString('ascii');
33 | };
34 |
35 | module.exports = {
36 | encrypt,
37 | decrypt,
38 | decode
39 | };
40 |
--------------------------------------------------------------------------------
/app/angular/components/dropdownIcon.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
41 |
--------------------------------------------------------------------------------
/app/sass/sql.scss:
--------------------------------------------------------------------------------
1 | // body {
2 | // background: rgb(204,204,204);
3 | // }
4 | page {
5 | background: white;
6 | display: block;
7 | margin: 0 auto;
8 | margin-bottom: 0.5cm;
9 | box-shadow: 0 0 0.5cm rgba(0,0,0,0.5);
10 | }
11 | page[size="A4"] {
12 | width: 21cm;
13 | height: 29.7cm;
14 | }
15 | page[size="A4"][layout="portrait"] {
16 | width: 29.7cm;
17 | height: 21cm;
18 | }
19 | page[size="A3"] {
20 | width: 29.7cm;
21 | height: 42cm;
22 | }
23 | page[size="A3"][layout="portrait"] {
24 | width: 42cm;
25 | height: 29.7cm;
26 | }
27 | page[size="A5"] {
28 | width: 14.8cm;
29 | height: 21cm;
30 | }
31 | page[size="A5"][layout="portrait"] {
32 | width: 21cm;
33 | height: 14.8cm;
34 | }
35 | @media print {
36 | body, page {
37 | margin: 0;
38 | box-shadow: 0;
39 | }
40 | }
41 |
42 | .sqlpage {
43 | margin-top: 75px;
44 | }
45 |
46 | .sqlTitle {
47 | font-size: 24px;
48 | color: var(--brand-default);
49 | display: block;
50 | margin: 0 auto;
51 | text-align: center;
52 | margin: 20px;
53 | }
54 |
55 | .ta-editor {
56 | min-height: 300px;
57 | height: auto;
58 | overflow: auto;
59 | font-family: inherit;
60 | font-size: 100%;
61 | margin:20px 0;
62 | }
63 |
64 | .pageContent {
65 | font-size: 16px;
66 | padding: 35px;
67 | }
68 |
--------------------------------------------------------------------------------
/server_app/user/validator.js:
--------------------------------------------------------------------------------
1 | const MESSAGES = {
2 | MISSING_USERNAME: "Missig username param",
3 | MISSING_PASSWORD: "Missig password param",
4 | MISSING_MAIL: "Missig mail param",
5 | VALID_PARAM: "Everything ok with your params",
6 | };
7 |
8 | class ValidationResponse {
9 | constructor(isValid, message) {
10 | this.valid = isValid;
11 | this.message = message;
12 | }
13 | }
14 |
15 | const validateLoginParams = ({ username, password }) => {
16 | if (username == "") {
17 | return new ValidationResponse(false, MESSAGES.MISSING_USERNAME);
18 | }
19 |
20 | if (password == "") {
21 | return new ValidationResponse(false, MESSAGES.MISSING_PASSWORD);
22 | }
23 |
24 | return new ValidationResponse(true, MESSAGES.VALID_PARAM);
25 | };
26 |
27 | const validateSignUpParams = ({ username, mail, password }) => {
28 | if (username == "") {
29 | return new ValidationResponse(false, MESSAGES.MISSING_USERNAME);
30 | }
31 |
32 | if (mail == "") {
33 | return new ValidationResponse(false, MESSAGES.MISSING_MAIL);
34 | }
35 |
36 | if (password == "") {
37 | return new ValidationResponse(false, MESSAGES.MISSING_PASSWORD);
38 | }
39 |
40 | return new ValidationResponse(true, MESSAGES.VALID_PARAM);
41 | };
42 |
43 | module.exports = {
44 | validateLoginParams,
45 | validateSignUpParams,
46 | };
47 |
--------------------------------------------------------------------------------
/app/react/theme/index.ts:
--------------------------------------------------------------------------------
1 | const theme = {
2 | colors: {
3 | primary: {
4 | 500: "hsl(170, 100%, 30%)",
5 | },
6 | secondary: {
7 | 500: "hsl(170, 100%, 40%)",
8 | },
9 | gray: {
10 | 100: "hsl(0, 0%, 10%",
11 | },
12 | white: "hsl(0, 0%, 100%)",
13 | black: "hsl(0, 0, 0)",
14 | },
15 | shadows: {
16 | default: "0 0 20px rgb(0, 0, 0, 0.1)",
17 | },
18 | radii: {
19 | none: "0",
20 | sm: "0.125rem",
21 | default: "0.25rem",
22 | md: "0.375rem",
23 | lg: "0.5rem",
24 | xl: "0.75rem",
25 | "2xl": "1rem",
26 | "3xl": "1.5rem",
27 | full: "9999px",
28 | },
29 | spacings: {
30 | 0.5: "0.125rem",
31 | 1: "0.25rem",
32 | 1.5: "0.375rem",
33 | 2: "0.5rem",
34 | 2.5: "0.625rem",
35 | 3: "0.75rem",
36 | 3.5: "0.875rem",
37 | 4: "1rem",
38 | 5: "1.25rem",
39 | 6: "1.5rem",
40 | 7: "1.75rem",
41 | 8: "2rem",
42 | 9: "2.25rem",
43 | 10: "2.5rem",
44 | 11: "2.75rem",
45 | 12: "3rem",
46 | 14: "3.5rem",
47 | 16: "4rem",
48 | 20: "5rem",
49 | 24: "6rem",
50 | 28: "7rem",
51 | 32: "8rem",
52 | 36: "9rem",
53 | 40: "10rem",
54 | 44: "11rem",
55 | 48: "12rem",
56 | 52: "13rem",
57 | 56: "14rem",
58 | 60: "15rem",
59 | 64: "16rem",
60 | 72: "18rem",
61 | 80: "20rem",
62 | 96: "24rem",
63 | },
64 | };
65 |
66 | export default theme;
67 |
--------------------------------------------------------------------------------
/app/angular/components/createModelModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
32 |
33 |
--------------------------------------------------------------------------------
/app/angular/components/sqlComparasionDropdown/index.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./template.html";
3 |
4 | const Controller = function () {
5 | const $ctrl = this;
6 |
7 | $ctrl.select = selected => {
8 | $ctrl.selected = selected;
9 | $ctrl.onSelect({ selected });
10 | }
11 |
12 | $ctrl.checkConstraintOptions = {
13 | NUMBER: [
14 | { name: 'Equal to', type: 'EQUAL_TO' },
15 | { name: 'Not equal to', type: 'NOT_EQUAL_TO' },
16 | { name: 'Greater than', type: 'GREATER_THAN' },
17 | { name: 'Less than', type: 'LESS_THAN' },
18 | { name: 'Between', type: 'BETWEEN' },
19 | { name: 'Greater than or equal to', type: 'GREATER_THAN_OR_EQUAL_TO' },
20 | { name: 'Less than or equal to', type: 'LESS_THAN_OR_EQUAL_TO' },
21 | ],
22 | STRING: [
23 | { name: 'Is', type: 'IS' },
24 | { name: 'Is not', type: 'IS_NOT' },
25 | { name: 'Contains', type: 'CONTAINS' },
26 | { name: 'Does not contain', type: 'DOES_NOT_CONTAIN' },
27 | { name: 'Starts with', type: 'STARTS_WITH' },
28 | { name: 'Ends with', type: 'ENDS_WITH' },
29 | ]
30 | };
31 | };
32 |
33 | export default angular.module("app.sqlComparasionDropdown", []).component("sqlComparasionDropdown", {
34 | template,
35 | controller: Controller,
36 | bindings: {
37 | type: "<",
38 | selected: "<",
39 | onSelect: "&"
40 | },
41 | }).name;
42 |
--------------------------------------------------------------------------------
/app/sass/tables.scss:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////
2 | // , | adjustments
3 | ///////////////////////////////////////////////////////////////////////////////
4 | .table > thead > tr > th,
5 | .table > tbody > tr > td {
6 | vertical-align: middle;
7 | border-color: var(--border-default);
8 | }
9 |
10 | .table thead th strong {
11 | font-weight: 600;
12 | }
13 |
14 | ///////////////////////////////////////////////////////////////////////////////
15 | // .table-hover
16 | ///////////////////////////////////////////////////////////////////////////////
17 | .table-hover tbody tr {
18 | cursor: pointer;
19 | }
20 |
21 | ///////////////////////////////////////////////////////////////////////////////
22 | // .action-cell
23 | ///////////////////////////////////////////////////////////////////////////////
24 | .actions-cell {
25 | text-align: right;
26 | }
27 |
28 | .actions-cell .fa {
29 | pointer-events: none;
30 | opacity: 0;
31 | font-size: 1.3em;
32 | padding: 4px 6px;
33 | border: 1px solid transparent;
34 | border-radius: 2px;
35 | margin-left: 3px;
36 | }
37 |
38 | tr:hover .actions-cell .fa {
39 | pointer-events: all;
40 | opacity: 1;
41 | border-radius: 4px;
42 | }
43 |
44 | .actions-cell .fa:hover {
45 | border-color: var(--border-default);
46 | background-color: var(--gray-90);
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/app/angular/logic/columnForm.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./columnForm.html";
3 |
4 | const app = angular.module("app.columnForm", []);
5 |
6 | const Controller = function () {
7 | const $ctrl = this;
8 |
9 | $ctrl.columnTypes = [
10 | { name: 'DATE', type: 'DATE' },
11 | { name: 'DATETIME', type: 'DATETIME' },
12 | { name: 'TIME', type: 'TIME' },
13 | { name: 'INT', type: 'INT' },
14 | { name: 'BOOLEAN', type: 'BOOLEAN' },
15 | { name: 'FLOAT', type: 'FLOAT' },
16 | { name: 'VARCHAR', type: 'VARCHAR' },
17 | { name: 'CHAR', type: 'CHAR' },
18 | { name: 'JSON', type: 'JSON' },
19 | { name: 'ENUM', type: 'ENUM' },
20 | { name: 'SET', type: 'SET'}
21 | ];
22 |
23 | $ctrl.cancel = () => {
24 | $ctrl.dismiss();
25 | };
26 |
27 | $ctrl.selectType = function (selected) {
28 | if (!$ctrl.column.PK && !$ctrl.column.FK) {
29 | $ctrl.column.type = selected.type;
30 | } else {
31 | $ctrl.column.type = "INT";
32 | }
33 | }
34 |
35 | $ctrl.selectTableOrigin = function (selected) {
36 | $ctrl.column.tableOrigin.idName = selected.name;
37 | }
38 | };
39 |
40 | export default app.component("columnForm", {
41 | template: template,
42 | bindings: {
43 | save: "<",
44 | dismiss: "&",
45 | column: "<",
46 | index: "<",
47 | tableNames: "<",
48 | delete: "<"
49 | },
50 | controller: Controller,
51 | }).name;
52 |
--------------------------------------------------------------------------------
/app/angular/components/createModelModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./createModelModal.html";
3 |
4 | const app = angular.module("app.createModelModal", []);
5 |
6 | const Controller = function ($rootScope, $filter) {
7 | const $ctrl = this;
8 | $ctrl.typeSelected = {};
9 | $ctrl.submitted = false;
10 |
11 | $ctrl.options = [
12 | { name: $filter('translate')('Conceptual'), type: 'conceptual'},
13 | { name: $filter('translate')('Logical'), type: 'logic' }
14 | ];
15 |
16 | $ctrl.$onInit = () => {
17 | $ctrl.typeSelected = { name: $filter('translate')("Conceptual"), type: "conceptual" };
18 | };
19 |
20 | $ctrl.doSelectType = function (newSelectedType) {
21 | $ctrl.typeSelected = newSelectedType;
22 | };
23 |
24 | $ctrl.save = function (modelname) {
25 | $ctrl.submitted = true;
26 | if (modelname != null && modelname != "") {
27 | const newmodel = {
28 | name: modelname,
29 | user: $rootScope.loggeduser,
30 | type: $ctrl.typeSelected.type,
31 | model: '{"cells":[]}',
32 | };
33 | $ctrl.close({ result: newmodel });
34 | }
35 | };
36 |
37 | $ctrl.cancel = function () {
38 | $ctrl.dismiss({
39 | reason: "cancel",
40 | });
41 | };
42 | };
43 | export default app.component("createModelModal", {
44 | template: template,
45 | bindings: {
46 | close: "&",
47 | dismiss: "&",
48 | },
49 | controller: Controller,
50 | }).name;
51 |
--------------------------------------------------------------------------------
/app/react/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "airbnb-base",
5 | "eslint:recommended",
6 | "plugin:prettier/recommended",
7 | "plugin:react/recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended",
10 | "prettier/@typescript-eslint"
11 | ],
12 | "parser": "@typescript-eslint/parser",
13 | "env": {
14 | "browser": true,
15 | "es6": true,
16 | "jest": true
17 | },
18 | "parserOptions": {
19 | "ecmaVersion": 2020,
20 | "sourceType": "module",
21 | "ecmaFeatures": {
22 | "jsx": true,
23 | "arrowFunctions": true
24 | }
25 | },
26 | "plugins": ["react", "@typescript-eslint", "prettier"],
27 | "settings": {
28 | "react": {
29 | "version": "detect"
30 | },
31 | "import/resolver": {
32 | "typescript": {
33 | "alwaysTryTypes": true,
34 | "project": "app/react/tsconfig.json"
35 | }
36 | }
37 | },
38 | "rules": {
39 | "no-shadow": "off",
40 | "react/jsx-uses-react": "off",
41 | "react/react-in-jsx-scope": "off",
42 | "@typescript-eslint/no-shadow": ["error"],
43 | "react/jsx-filename-extension": [
44 | 1,
45 | {
46 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
47 | }
48 | ],
49 | "import/extensions": [
50 | "error",
51 | "ignorePackages",
52 | {
53 | "js": "never",
54 | "jsx": "never",
55 | "ts": "never",
56 | "tsx": "never"
57 | }
58 | ]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/server_app/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const errorhandler = require("errorhandler");
3 | const session = require("express-session");
4 | const bodyParser = require("body-parser");
5 | const path = require("path");
6 | const cors = require("cors");
7 | const enforce = require('express-sslify');
8 |
9 | require("dotenv").config();
10 |
11 | let app = express();
12 | app.use(cors());
13 |
14 | if (app.get("env") === "production") {
15 | app.use(enforce.HTTPS({ trustProtoHeader: true }));
16 | }
17 |
18 | // Where to find the view files
19 | const viewsPath = path.join(__dirname, "../views");
20 | app.set("views", viewsPath);
21 |
22 | app.use(bodyParser.json({limit: '50mb'})); // support json encoded bodies
23 | app.use(bodyParser.urlencoded({ extended: true, limit: '50mb'})); // support encoded bodies
24 |
25 | const appPath = path.join(__dirname, "../app");
26 | app.use(express.static(`${appPath}/dist`));
27 |
28 | app.use(
29 | session({
30 | resave: true,
31 | saveUninitialized: true,
32 | secret: "SOMERANDOMSECRETHERE",
33 | cookie: { maxAge: 60000 },
34 | })
35 | );
36 | app.use(errorhandler());
37 |
38 | const userHandler = require("./user/handler");
39 | const modelHandler = require("./model/handler");
40 |
41 |
42 | app.use("/users", userHandler);
43 | app.use("/models", modelHandler);
44 |
45 | app.get("/", (_, res) => {
46 | res.render("index.html");
47 | });
48 |
49 | module.exports = app;
--------------------------------------------------------------------------------
/app/angular/logic/checkConstraint.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/angular/conceptual/validator.test.js:
--------------------------------------------------------------------------------
1 | import Validator from "./validator";
2 | import Factory from "./factory";
3 |
4 | import * as joint from "jointjs";
5 |
6 | import shapes from "../../joint/shapes";
7 | joint.shapes.erd = shapes;
8 |
9 | describe("Test shape erd validator", () => {
10 |
11 | const validator = new Validator();
12 | const factory = new Factory(joint.shapes);
13 |
14 | test("It should validate an Entity", async () => {
15 | const entity = factory.createEntity();
16 | expect(validator.isEntity(entity)).toBeTruthy()
17 | });
18 |
19 | test("It should validate a Relationship", async () => {
20 | const entity = factory.createRelationship();
21 | expect(validator.isRelationship(entity)).toBeTruthy()
22 | });
23 |
24 | test("It should validate an Attribute", async () => {
25 | const entity = factory.createAttribute();
26 | expect(validator.isAttribute(entity)).toBeTruthy()
27 | });
28 |
29 | test("It should validate a Extension", async () => {
30 | const entity = factory.createIsa();
31 | expect(validator.isExtension(entity)).toBeTruthy()
32 | });
33 |
34 | test("It should validate a Associative Entity", async () => {
35 | const entity = factory.createAssociative();
36 | expect(validator.isAssociative(entity)).toBeTruthy()
37 | });
38 |
39 | test("It should validate a Link", async () => {
40 | const entity = factory.createLink();
41 | expect(validator.isLink(entity)).toBeTruthy()
42 | });
43 |
44 | });
--------------------------------------------------------------------------------
/cypress/e2e/model.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe("Model", () => {
4 | beforeEach(() => {
5 | cy.intercept("GET", "/models?userId=*").as("getUserModels");
6 | cy.loginViaGui();
7 | cy.wait("@getUserModels").then((userModels) => {
8 | cy.cleanUpUserModels(userModels);
9 | cy.reload();
10 | cy.contains("a", "New model").click();
11 | cy.get("create-model-modal").should("be.visible");
12 | });
13 | });
14 |
15 | it('closes the "New Model" modal', () => {
16 | cy.contains("button", "Cancel").click();
17 | cy.get("create-model-modal").should("not.exist");
18 | });
19 |
20 | it("alerts when clicking SAVE without filling the title", () => {
21 | cy.contains("button", "Save").click();
22 | cy.get("#name").should("have.class", "error");
23 | });
24 |
25 | context("Model creation", () => {
26 | const modelTitle = "User";
27 |
28 | beforeEach(() => cy.get("#name").type(modelTitle));
29 |
30 | it("creates a conceptual model", () => {
31 | cy.contains("button", "Save").click();
32 |
33 | cy.contains("h2", `Conceptual model of: ${modelTitle}`).should(
34 | "be.visible"
35 | );
36 | });
37 |
38 | it("creates a logical model", () => {
39 | // Selects the logical model and save
40 | cy.get(".modelselect").click();
41 | cy.contains("li span", "Logical").click();
42 | cy.contains("button", "Save").click();
43 | // Asserts the model was created
44 | cy.contains("h2", `Logical model of: ${modelTitle}`).should("be.visible");
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/app/angular/recovery/recovery.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
9 |
10 |
11 | {{ 'Recovery password' | translate }}
12 |
13 |
14 | {{ $ctrl.feedback.message }}
15 |
16 |
33 |
39 |
40 |
--------------------------------------------------------------------------------
/app/angular/login/login.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import ReactButton from "../../react/components/Button";
3 | import authService from "../service/authService";
4 | import template from "./login.html";
5 |
6 | const LoginController = function (AuthService, $state, $translate, $filter) {
7 | const ctrl = this;
8 | ctrl.submitted = false;
9 | ctrl.credentials = {};
10 | ctrl.feedback = {
11 | message: "",
12 | showing: false,
13 | };
14 |
15 | const showError = (newMessage) => {
16 | ctrl.feedback.message = $filter('translate')(newMessage);
17 | ctrl.feedback.showing = true;
18 | };
19 |
20 | const handleLoginError = () => {
21 | ctrl.loading = false;
22 | showError("Incorrect login or password");
23 | };
24 |
25 | const handleLoginSuccess = () => {
26 | ctrl.loading = false;
27 | $state.go("main");
28 | };
29 |
30 | const doLogin = () => {
31 | ctrl.loading = true;
32 | AuthService.login(ctrl.credentials)
33 | .then(handleLoginSuccess)
34 | .catch(handleLoginError);
35 | };
36 |
37 | ctrl.changeLanguage = (langKey) => {
38 | localStorage.setItem('i18n', langKey);
39 | $translate.use(langKey);
40 | };
41 |
42 | ctrl.submitForm = (validForm) => {
43 | ctrl.submitted = true;
44 | ctrl.feedback.showing = false;
45 | if (validForm) {
46 | doLogin();
47 | } else {
48 | showError("Fill the fields in red");
49 | }
50 | };
51 | };
52 |
53 | export default angular
54 | .module("app.login", [authService, ReactButton])
55 | .component("login", {
56 | template,
57 | controller: LoginController,
58 | }).name;
--------------------------------------------------------------------------------
/app/angular/service/preventExitService.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import confirmationModal from "../components/confirmationModal";
3 |
4 | const preventExitService = ($uibModal, $filter) => {
5 | const __handleBeforeUnload = (scope) => (event) => {
6 | if (scope.modelState.isDirty) return "";
7 | event.stopPropagation();
8 | };
9 |
10 | const __handleTransitionStart = (scope, name) => async (trans) => {
11 | if (trans.from().name === name && scope.modelState.isDirty) {
12 | const modalInstance = $uibModal.open({
13 | animation: true,
14 | component: "confirmationModal",
15 | resolve: {
16 | modalData: () => ({
17 | title: $filter('translate')("Unsaved changes"),
18 | content: $filter('translate')("You have unsaved changes. Are you sure you want to exit without saving?"),
19 | cancelLabel: $filter('translate')("Cancel"),
20 | confirmLabel: $filter('translate')("Exit without saving"),
21 | }),
22 | },
23 | });
24 |
25 | const result = await modalInstance.result;
26 | return result;
27 | }
28 | return true;
29 | };
30 |
31 | const __cleanup = (scope) => () => {
32 | window.onbeforeunload = null;
33 | // eslint-disable-next-line no-param-reassign
34 | scope.modelState = {};
35 | };
36 |
37 | return {
38 | handleBeforeUnload: __handleBeforeUnload,
39 | handleTransitionStart: __handleTransitionStart,
40 | cleanup: __cleanup,
41 | };
42 | };
43 |
44 | export default angular
45 | .module("app.preventExit", [confirmationModal])
46 | .factory("preventExitService", preventExitService).name;
47 |
--------------------------------------------------------------------------------
/app/angular/components/importModelModal.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
38 |
39 |
--------------------------------------------------------------------------------
/app/angular/recovery/recovery.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./recovery.html";
3 | import AuthService from "../service/authService"
4 |
5 | const controller = function (AuthService, $filter) {
6 | const ctrl = this;
7 | ctrl.submitted = false;
8 | ctrl.mail = "";
9 | ctrl.feedback = {
10 | message: "",
11 | showing: false,
12 | type: "error"
13 | };
14 |
15 | const showFeedback = (newMessage, type = "error") => {
16 | ctrl.feedback.message = $filter('translate')(newMessage);
17 | ctrl.feedback.showing = true;
18 | ctrl.feedback.type = type;
19 | };
20 |
21 | const handleError = (error) => {
22 | ctrl.loading = false;
23 | if(error.status == 400) {
24 | showFeedback("This email is not registered yet.");
25 | } else {
26 | showFeedback("Ops, we had an internal error, please try again later...");
27 | }
28 | };
29 |
30 | const handleSuccess = () => {
31 | ctrl.loading = false;
32 | showFeedback($filter('translate')('An email with instructions has been sent to EMAIL', { email: ctrl.mail }), "success");
33 | };
34 |
35 | const doRecover = () => {
36 | ctrl.loading = true;
37 | AuthService.recovery(ctrl.mail)
38 | .then(handleSuccess)
39 | .catch(handleError);
40 | };
41 |
42 | ctrl.submitForm = (validForm) => {
43 | ctrl.submitted = true;
44 | ctrl.feedback.showing = false;
45 | if (validForm) {
46 | doRecover();
47 | } else {
48 | showFeedback("Fill the fields in red");
49 | }
50 | };
51 | };
52 |
53 |
54 | export default angular
55 | .module("app.recovery", [AuthService])
56 | .component("recovery", {
57 | template,
58 | controller,
59 | }).name;
--------------------------------------------------------------------------------
/server_app/model/validator.js:
--------------------------------------------------------------------------------
1 | const MESSAGES = {
2 | MISSING_NAME: "Missig model name param",
3 | WRONG_NAME_TYPE: "Name should be a String",
4 | MISSING_TYPE: "Missig type param",
5 | WRONG_TYPE_TYPE: "Type should be a String",
6 | MISSING_MODEL: "Missig model param",
7 | WRONG_MODEL_TYPE: "Model should be an Object",
8 | MISSING_USER: "Missig userId param",
9 | WRONG_USER_TYPE: "UserId should be an Object",
10 | };
11 |
12 | class ValidationResponse {
13 | constructor(isValid, message) {
14 | this.valid = isValid;
15 | this.message = message;
16 | }
17 | }
18 |
19 | const getMessages = () => {
20 | return MESSAGES;
21 | };
22 |
23 | const isString = (value) => typeof value === "string" || value instanceof String;
24 | const isNull = (value) => value == null || value == "";
25 |
26 | const validateSaveParams = ({ name, type, model, userId }) => {
27 | if (isNull(name)) {
28 | return new ValidationResponse(false, MESSAGES.MISSING_NAME);
29 | }
30 |
31 | if (!isString(name)) {
32 | return new ValidationResponse(false, MESSAGES.WRONG_NAME_TYPE);
33 | }
34 |
35 | if (isNull(type)) {
36 | return new ValidationResponse(false, MESSAGES.MISSING_TYPE);
37 | }
38 |
39 | if (!isString(type)) {
40 | return new ValidationResponse(false, MESSAGES.WRONG_TYPE_TYPE);
41 | }
42 |
43 | if (isNull(userId)) {
44 | return new ValidationResponse(false, MESSAGES.MISSING_USER);
45 | }
46 |
47 | if (!isString(userId)) {
48 | return new ValidationResponse(false, MESSAGES.WRONG_USER_TYPE);
49 | }
50 |
51 | return new ValidationResponse(true, MESSAGES.VALID_PARAM);
52 | };
53 |
54 | module.exports = {
55 | validateSaveParams,
56 | getMessages,
57 | };
58 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 |
2 | import { faker } from '@faker-js/faker/locale/en';
3 |
4 | Cypress.Commands.add(
5 | "loginViaGui",
6 | (user = Cypress.env("user"), password = Cypress.env("password")) => {
7 | cy.visit("/");
8 | cy.get("#userEmail").type(user);
9 | cy.get("#userPassword").type(password, { log: false });
10 | cy.contains("button", "Login").click();
11 | }
12 | );
13 |
14 | Cypress.Commands.add("cleanUpUserModels", (userModels) => {
15 | cy.getUserModelsViaApi(userModels.response.url).then(
16 | (userModelsResponseUrl) => {
17 | userModelsResponseUrl.body.forEach((model) => {
18 | cy.deleteModelViaApi(model._id);
19 | });
20 | }
21 | );
22 | });
23 |
24 | Cypress.Commands.add("getUserModelsViaApi", (url) => {
25 | cy.getCookie('userToken').then(({ value }) => {
26 | cy.request({
27 | method: "GET",
28 | url,
29 | headers: { "brx-access-token": value },
30 | });
31 | });
32 | });
33 |
34 | Cypress.Commands.add("deleteModelViaApi", (modelId) => {
35 | cy.getCookie('userToken').then(({ value }) => {
36 | cy.request({
37 | method: "DELETE",
38 | url: `${Cypress.env("apiUrl")}/models/:modelId?modelId=${modelId}`,
39 | headers: { "brx-access-token": value },
40 | });
41 | });
42 | });
43 |
44 | Cypress.Commands.add(
45 | "createModelViaApi",
46 | (type, userId, model = { cell: [] }) => {
47 | cy.getCookie('userToken').then(({ value }) => {
48 | cy.request({
49 | method: "POST",
50 | url: `${Cypress.env("apiUrl")}/models`,
51 | headers: { "brx-access-token": value },
52 | body: {
53 | name: faker.animal.type(),
54 | user: userId,
55 | type,
56 | model,
57 | },
58 | });
59 | });
60 | }
61 | );
62 |
--------------------------------------------------------------------------------
/app/angular/components/importModelModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./importModelModal.html";
3 |
4 | const app = angular.module("app.importModelModal", []);
5 |
6 | const Controller = function (ModelAPI, AuthService, $timeout) {
7 | const $ctrl = this;
8 | const urlRegex = /^(https?:\/\/(?:[\w.-]+|\[::1\]|localhost)(?::\d{1,5})?\/#!\/publicview\/[\w.-]+)$/;;
9 |
10 | const validateUrl = (url) => {
11 | if(url == null || url == "") {
12 | return false
13 | }
14 | return urlRegex.test(url);
15 | }
16 |
17 | $ctrl.$onInit = () => {
18 | $ctrl.url = "";
19 | $ctrl.submitted = false;
20 | $ctrl.valid = false;
21 | $ctrl.loading = false;
22 | $ctrl.feedback = {
23 | showing: false
24 | }
25 | };
26 |
27 | $ctrl.cancel = () => {
28 | $ctrl.dismiss({
29 | reason: "cancel",
30 | });
31 | };
32 |
33 | $ctrl.showErrorFeedback = () => {
34 | $timeout(() => {
35 | $ctrl.feedback.showing = true;
36 | })
37 | };
38 |
39 | $ctrl.save = (url) => {
40 | const parts = url.split("/");
41 | const shareId = parts[parts.length-1];
42 | $ctrl.submitted = true;
43 | $ctrl.valid = validateUrl(url);
44 | if($ctrl.valid) {
45 | $ctrl.loading = true;
46 | ModelAPI.importModel(shareId, AuthService.loggeduser).then(response => {
47 | $ctrl.loading = false;
48 | $ctrl.close({result: response.data});
49 | }).catch(error => {
50 | $ctrl.loading = false;
51 | console.log(error);
52 | $ctrl.showErrorFeedback();
53 | });
54 | }
55 | };
56 | };
57 |
58 | export default app.component("importModelModal", {
59 | template: template,
60 | bindings: {
61 | close: "&",
62 | dismiss: "&"
63 | },
64 | controller: Controller,
65 | }).name;
66 |
--------------------------------------------------------------------------------
/app/sass/note-color-list.scss:
--------------------------------------------------------------------------------
1 | .note-color-list {
2 | display: flex;
3 | gap: 1rem;
4 | list-style: none;
5 | padding: 0;
6 | }
7 |
8 | .note-color-list > li {
9 | position: relative;
10 | width: 32px;
11 | height: 32px;
12 | overflow: hidden;
13 | border: 1px solid transparent;
14 | border-radius: 8px;
15 | }
16 |
17 | li:has(#color-gray) {background-color: hsl(216deg, 25%, 74.9%, 1);} // #afbccf
18 | li:has(#color-salmon) {background-color: hsl(7.13deg, 94.4%, 79%, 1);} // #fca397
19 | li:has(#color-orange) {background-color: hsl(35.2deg, 100%, 72%, 1);} // #ffc470
20 | li:has(#color-yellow) {background-color: hsl(60deg, 100%, 76.7%, 1);} // #ffff88
21 | li:has(#color-green) {background-color: hsl(140deg, 49.7%, 64.9%, 1);} // #79d297
22 | li:has(#color-blue) {background-color: hsl(205deg, 89.9%, 72.9%, 1);} // #7cc4f8
23 | li:has(#color-purple) {background-color: hsl(268deg, 100%, 82.9%, 1);} // #d1a8ff
24 | li:has(#color-pink) {background-color: hsl(318deg, 96%, 80.2%, 1);} // #fd9ce0
25 |
26 | .note-color-list > li label {
27 | position: absolute;
28 | top: 32px
29 | }
30 |
31 | .note-color-list > li [type="radio"] {
32 | display: block;
33 | position: absolute;
34 | // inset: 0;
35 | // Use inset on type="radio" causes issues on Firefox
36 | // Due to that I'm forcing width and height manualy
37 | width: 100%;
38 | height: 100%;
39 | margin: 0;
40 | padding: 0;
41 | opacity: 0;
42 | cursor: pointer;
43 | }
44 |
45 | .note-color-list > li:focus-within{
46 | outline: dashed var(--brand-primary-10);
47 | }
48 |
49 | .note-color-list > li:has(:checked):after {
50 | display: block;
51 | z-index: 1;
52 | content: "✔︎";
53 | font-size: 1.5em;
54 | text-align: center;
55 | color: var(--gray-30);
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/app/sass/modelWorkspace.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .modelWorkspace .navbar
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .modelWorkspace .navbar {
5 | width: 150px;
6 | border-right: 1px solid var(--border-default);
7 | border-bottom: 1px solid var(--border-default);
8 | background-color: var(--gray-95)
9 | }
10 |
11 | .modelWorkspace .navbar .back-button {
12 | font-size: 1.2em;
13 | color: var(--brand-default);
14 | padding-top: 16px;
15 | padding-bottom: 17px;
16 | border-right: 1px solid var(--border-default);
17 | }
18 |
19 | .modelWorkspace .navbar .back-button:hover {
20 | color: var(--brand-default);
21 | background-color: #e1e1e1;
22 | }
23 |
24 | .modelWorkspace .navbar-header {
25 | float: none;
26 | }
27 |
28 | .modelWorkspace .navbar-header .navbar-brand {
29 | float: none;
30 | display: block;
31 | padding: 15px 0;
32 | text-align: center;
33 | }
34 |
35 | ////////////////////////////////////////////////////////////////////////////////
36 | // .modelWorkspace .mainContent
37 | ////////////////////////////////////////////////////////////////////////////////
38 | .modelWorkspace .mainContent {
39 | padding-top: 0;
40 | }
41 |
42 | .modelWorkspace .model-area {
43 | top: 0;
44 | }
45 |
46 | .modelWorkspace .workspace-header .page-header {
47 | margin-top: 10px;
48 | border-bottom: 1px solid var(--border-default);
49 | }
50 |
51 | ////////////////////////////////////////////////////////////////////////////////
52 | // .share-status
53 | ////////////////////////////////////////////////////////////////////////////////
54 | .share-status {
55 | color: var(--gray-70);
56 | font-size: 1.2rem;
57 | margin-right: 4px;
58 | }
--------------------------------------------------------------------------------
/app/angular/components/shareModelModal.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./shareModelModal.html";
3 |
4 | const app = angular.module("app.shareModelModal", []);
5 |
6 | const Controller = function (ModelAPI) {
7 | const $ctrl = this;
8 | $ctrl.submitted = false;
9 |
10 | $ctrl.$onInit = () => {
11 | ModelAPI.loadShareOptions($ctrl.modelId).then(response => {
12 | const shareConfig = response.data;
13 | $ctrl.url = shareConfig.active ? shareConfig.url : "";
14 | $ctrl.shared = shareConfig.active;
15 | $ctrl.importAllowed = shareConfig.importAllowed;
16 | $ctrl.backupConfig = shareConfig;
17 | });
18 | };
19 |
20 | const hasChanges = (newActive, newImportAllowed) => {
21 | return $ctrl.backupConfig.active != newActive || $ctrl.backupConfig.importAllowed != newImportAllowed;
22 | }
23 |
24 | $ctrl.toggleShare = (shared) => {
25 | $ctrl.url = shared? $ctrl.backupConfig.url : "";
26 | if (!shared) {
27 | $ctrl.importAllowed = false;
28 | }
29 | }
30 |
31 | $ctrl.cancel = () => {
32 | $ctrl.dismiss({reason: "cancel"});
33 | };
34 |
35 | $ctrl.save = () => {
36 | if(hasChanges($ctrl.shared, $ctrl.importAllowed)) {
37 | ModelAPI.toggleShare($ctrl.modelId, $ctrl.shared, $ctrl.importAllowed).then(() => {
38 | $ctrl.close({result: {'shared': $ctrl.shared}});
39 | }).catch(error => {
40 | console.log(error);
41 | $ctrl.dismiss({reason: "model share error"});
42 | });
43 | } else {
44 | $ctrl.close({reason: "model shared"});
45 | }
46 | };
47 |
48 | $ctrl.copy = () => {
49 | navigator.clipboard.writeText($ctrl.url);
50 | };
51 | };
52 |
53 | export default app.component("shareModelModal", {
54 | template: template,
55 | bindings: {
56 | close: "&",
57 | dismiss: "&",
58 | modelId: "<",
59 | },
60 | controller: Controller,
61 | }).name;
--------------------------------------------------------------------------------
/app/angular/signup/signup.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import authService from "../service/authService";
3 | import template from "./signup.html";
4 |
5 | const SignupController = function ($state, AuthService, $filter) {
6 | const ctrl = this;
7 | ctrl.submitted = false;
8 | ctrl.loading = false;
9 | ctrl.credentials = {
10 | username: "",
11 | email: "",
12 | password: "",
13 | };
14 | ctrl.feedback = {
15 | message: "",
16 | showing: false,
17 | };
18 |
19 | const showLoading = (loading) => {
20 | ctrl.loading = loading;
21 | };
22 |
23 | const handleLoginError = () => {
24 | showLoading(false);
25 | $state.go("login");
26 | };
27 |
28 | const handleLoginSuccess = () => {
29 | showLoading(false);
30 | $state.go("main");
31 | };
32 |
33 | const handleSignUpSuccess = () => {
34 | AuthService.login({
35 | username: ctrl.credentials.email,
36 | password: ctrl.credentials.password,
37 | })
38 | .then(handleLoginSuccess)
39 | .catch(handleLoginError);
40 | };
41 |
42 | const handleSignUpError = (error) => {
43 | showLoading(false);
44 | showError(error.data);
45 | };
46 |
47 | const doRegister = () => {
48 | showLoading(true);
49 | AuthService.register(ctrl.credentials)
50 | .then(handleSignUpSuccess)
51 | .catch(handleSignUpError);
52 | };
53 |
54 | const showError = (newMessage) => {
55 | ctrl.feedback.message = $filter('translate')(newMessage);
56 | ctrl.feedback.showing = true;
57 | };
58 |
59 | ctrl.submitForm = (validForm) => {
60 | ctrl.submitted = true;
61 | ctrl.feedback.showing = false;
62 | if (validForm) {
63 | doRegister();
64 | } else {
65 | showError("Fill the fields in red");
66 | }
67 | };
68 | };
69 |
70 | export default angular.module("app.signup", [authService]).component("signup", {
71 | template,
72 | controller: SignupController,
73 | }).name;
--------------------------------------------------------------------------------
/server_app/user/validator.test.js:
--------------------------------------------------------------------------------
1 | const userValidator = require("./validator");
2 |
3 | describe('validateLoginParams test', () => {
4 | test('should fail when username is missing', () => {
5 | const validation = userValidator.validateLoginParams({username: "", password: "123456"});
6 | expect(validation.valid).toBeFalsy();
7 | });
8 |
9 | test('should fail when password is missing', () => {
10 | const validation = userValidator.validateLoginParams({username: "some_username", password: ""});
11 | expect(validation.valid).toBeFalsy();
12 | });
13 |
14 | test('should pass when when we have params all ok', () => {
15 | const validation = userValidator.validateLoginParams({username: "some_username", password: "some_password"});
16 | expect(validation.valid).toBeTruthy();
17 | });
18 | });
19 |
20 | describe('validateSignUpParams test', () => {
21 | test('should fail when username is missing', () => {
22 | const validation = userValidator.validateSignUpParams({username: "", mail: "", password: "123456"});
23 | expect(validation.valid).toBeFalsy();
24 | });
25 |
26 | test('should fail when mail is missing', () => {
27 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "", password: ""});
28 | expect(validation.valid).toBeFalsy();
29 | });
30 |
31 | test('should fail when password is missing', () => {
32 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "some_mail", password: ""});
33 | expect(validation.valid).toBeFalsy();
34 | });
35 |
36 | test('should pass when when we have params all ok', () => {
37 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "some_mail", password: "some_password"});
38 | expect(validation.valid).toBeTruthy();
39 | });
40 | });
--------------------------------------------------------------------------------
/cypress/fixtures/conceptualModel.json:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "type": "erd.Entity",
5 | "supertype": "Entity",
6 | "isExtended": false,
7 | "autorelationship": false,
8 | "size": { "width": 80, "height": 40 },
9 | "position": { "x": 170, "y": 50 },
10 | "angle": 0,
11 | "id": "bdb20ca0-5c43-40b2-94dd-d0aecfa5b856",
12 | "z": 1,
13 | "attrs": {}
14 | },
15 | {
16 | "type": "erd.Relationship",
17 | "supertype": "Relationship",
18 | "autorelationship": false,
19 | "size": { "width": 85, "height": 45 },
20 | "position": { "x": 330, "y": 50 },
21 | "angle": 0,
22 | "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3",
23 | "z": 2,
24 | "attrs": {}
25 | },
26 | {
27 | "type": "erd.Link",
28 | "source": { "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3" },
29 | "target": { "id": "bdb20ca0-5c43-40b2-94dd-d0aecfa5b856" },
30 | "id": "7ad0455f-5a16-46f4-bc69-dc1090a55978",
31 | "z": 3,
32 | "labels": [{ "position": 0.7, "attrs": { "text": { "text": "(0, n)" } } }],
33 | "attrs": {}
34 | },
35 | {
36 | "type": "erd.Entity",
37 | "supertype": "Entity",
38 | "isExtended": false,
39 | "autorelationship": false,
40 | "size": { "width": 80, "height": 40 },
41 | "position": { "x": 510, "y": 60 },
42 | "angle": 0,
43 | "id": "1846af3e-6e30-4471-ba8b-6829cab94050",
44 | "z": 4,
45 | "attrs": {}
46 | },
47 | {
48 | "type": "erd.Link",
49 | "source": { "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3" },
50 | "target": { "id": "1846af3e-6e30-4471-ba8b-6829cab94050" },
51 | "id": "69c1eca8-34f7-4ec0-8855-93f12f1ebc4f",
52 | "z": 5,
53 | "labels": [{ "position": 0.7, "attrs": { "text": { "text": "(0, n)" } } }],
54 | "attrs": {}
55 | }
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/app/angular/conceptual/entityExtensor.js:
--------------------------------------------------------------------------------
1 |
2 | export default class EntityExtensor {
3 |
4 | constructor(shapeFactory, shapeValidator, linker) {
5 | this.factory = shapeFactory;
6 | this.validator = shapeValidator;
7 | this.linker = linker;
8 | }
9 |
10 | createExtension = (entity, extentionType) => {
11 | const x = entity.model.attributes.position.x;
12 | const y = entity.model.attributes.position.y;
13 |
14 | const isa = this.factory.createIsa();
15 | const extendedEntity = this.factory.createEntity();
16 |
17 | isa.attributes.position.x = x + 18;
18 | isa.attributes.position.y = y + 60;
19 | isa.attributes.attrs.text.text = extentionType;
20 |
21 | extendedEntity.attributes.position.x = x;
22 | extendedEntity.attributes.position.y = y + 120;
23 |
24 | const graph = entity.model.graph;
25 |
26 | graph.addCell(isa);
27 | graph.addCell(extendedEntity);
28 |
29 | this.linker.createLink(isa, entity.model, graph);
30 |
31 | entity.model.attributes.isExtended = true;
32 | isa.attributes.parentId = entity.model.attributes.id;
33 |
34 | this.linker.createLink(isa, extendedEntity, graph);
35 | };
36 |
37 | updateExtension = (entity, extentionType) => {
38 | const graph = entity.model.graph;
39 | const paper = entity.paper;
40 |
41 | const neighbors = graph.getNeighbors(entity.model);
42 | const extension = neighbors.find(element => {
43 | return this.validator.isExtension(element) && element.attributes.parentId == entity.model.attributes.id;
44 | });
45 |
46 | if(extension != null) {
47 | extension.attributes.attrs.text.text = extentionType;
48 | const extensionView = extension.findView(paper);
49 | extensionView.update();
50 | }
51 |
52 | return extension;
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # System files
3 | ###############################################################################
4 | .idea
5 | .DS_Store
6 | .env
7 | .vscode
8 |
9 | ###############################################################################
10 | # node-waf configuration
11 | ###############################################################################
12 | .lock-wscript
13 | npm-debug.log
14 |
15 | ###############################################################################
16 | # Dependency directories
17 | ###############################################################################
18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
19 | node_modules/
20 | bower_components/
21 | build/
22 | package-lock.json
23 | coverage/
24 |
25 | ###############################################################################
26 | # Logs
27 | ###############################################################################
28 | yarn-error.log
29 |
30 | ###############################################################################
31 | # Compiled files
32 | ###############################################################################
33 | app/css
34 | app/assets
35 | app/dist
36 |
37 | ###############################################################################
38 | # ENV files
39 | ###############################################################################
40 | .env
41 | .env.development
42 | .env.production
43 |
44 | ###############################################################################
45 | # docker files
46 | ###############################################################################
47 | data/
48 | coverage/
49 |
50 | ###############################################################################
51 | # cypress
52 | ###############################################################################
53 | cypress.env.json
54 | cypress/downloads/
55 | cypress/screenshots/
56 | cypress/videos/
57 |
--------------------------------------------------------------------------------
/app/angular/components/shareModelModal.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
47 |
48 |
--------------------------------------------------------------------------------
/app/angular/service/logicFactory.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 |
3 | import * as joint from "jointjs/dist/joint";
4 | import "jointjs/dist/joint.min.css";
5 |
6 | const logicFactory = ($filter) => {
7 | var logic = joint.shapes.uml;
8 |
9 | const _createTable = function () {
10 | var table = new logic.Class({
11 | position: { x: 12, y: 15 },
12 | size: { width: 100, height: 100 },
13 | name: $filter('translate')('Table'),
14 | attributes: [],
15 | attrs: {
16 | '.uml-class-name-rect': {
17 | fill: '#fff',
18 | stroke: '#000',
19 | 'stroke-width': 0.5,
20 | },
21 | '.uml-class-attrs-rect': {
22 | fill: '#fff',
23 | stroke: '#000',
24 | 'stroke-width': 0.5
25 | },
26 | '.uml-class-methods-rect': {
27 | fill: '#fff',
28 | stroke: '#000',
29 | 'stroke-width': 0.5
30 | },
31 | '.uml-class-attrs-text': {
32 | ref: '.uml-class-attrs-rect',
33 | 'ref-y': 0.5,
34 | 'y-alignment': 'middle'
35 | }
36 | }
37 | });
38 | return table;
39 | }
40 |
41 | const _createView = function () {
42 | var table = new logic.Abstract({
43 | position: { x: 12, y: 155 },
44 | size: { width: 100, height: 100 },
45 | name: $filter('translate')('View'),
46 | attributes: [],
47 | attrs: {
48 | '.uml-class-name-rect': {
49 | fill: 'lightgray',
50 | stroke: '#000',
51 | 'stroke-width': 0.5,
52 | },
53 | '.uml-class-attrs-rect': {
54 | fill: '#fff',
55 | stroke: '#000',
56 | 'stroke-width': 0.5
57 | },
58 | '.uml-class-methods-rect': {
59 | fill: '#fff',
60 | stroke: '#000',
61 | 'stroke-width': 0.5
62 | },
63 | '.uml-class-attrs-text': {
64 | ref: '.uml-class-attrs-rect',
65 | 'ref-y': 0.5,
66 | 'y-alignment': 'middle'
67 | }
68 | }
69 | });
70 | return table;
71 | }
72 |
73 | return {
74 | createTable: _createTable,
75 | createView: _createView
76 | }
77 |
78 | }
79 |
80 | export default angular
81 | .module("app.LogicFactory", [])
82 | .factory("LogicFactory", logicFactory).name;
83 |
--------------------------------------------------------------------------------
/app/joint/notes.js:
--------------------------------------------------------------------------------
1 | import * as joint from "jointjs/dist/joint";
2 | import _ from "lodash";
3 |
4 | const Note = joint.dia.Element.extend({
5 | markup: '',
6 | defaults: _.defaultsDeep(
7 | {
8 | type: "custom.Note",
9 | supertype: "Note",
10 | size: { width: 80, height: 40 },
11 | attrs: {
12 | ".outer": {
13 | fill: "#afbccf",
14 | stroke: "#afbccf",
15 | "stroke-width": 1,
16 | points: "100,0 100,60 0,60 0,0",
17 | },
18 | ".inner": {
19 | fill: "#79d297",
20 | stroke: "#27AE60",
21 | "stroke-width": 1,
22 | points: "95,5 95,55 5,55 5,5",
23 | display: "none",
24 | },
25 | text: {
26 | text: "Nota",
27 | textWrap: {
28 | width: 300,
29 | height: 210,
30 | ellipsis: true,
31 | },
32 | "font-family": "Arial",
33 | "font-size": 14,
34 | ref: ".outer",
35 | "ref-x": 0.5,
36 | "ref-y": 0.5,
37 | "x-alignment": "middle",
38 | "y-alignment": "middle",
39 | },
40 | },
41 | },
42 | joint.dia.Element.prototype.defaults
43 | ),
44 | getType() {
45 | return "custom.Note";
46 | }
47 | });
48 |
49 | const NoteView = joint.dia.ElementView.extend({
50 | initialize: function () {
51 | joint.dia.ElementView.prototype.initialize.apply(this, arguments);
52 | },
53 | setText: function (newText) {
54 | this.model.attributes.attrs.text.text = newText;
55 | this.update();
56 | const textBox = this.$el.find("text")[0].getBBox();
57 | if (textBox.width > 80) {
58 | this.model.attributes.size.width = textBox.width + 10;
59 | this.resize();
60 | }
61 | if (textBox.height > 40) {
62 | this.model.attributes.size.height = textBox.height + 10;
63 | this.resize();
64 | }
65 | },
66 | setColor: function (newColor) {
67 | this.model.attributes.attrs[".outer"].fill = newColor;
68 | this.model.attributes.attrs[".outer"].stroke = newColor;
69 | this.update();
70 | }
71 | });
72 |
73 | export default {
74 | Note,
75 | NoteView
76 | };
--------------------------------------------------------------------------------
/app/angular/preferences/preferences.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 | 
32 |
33 |
34 | {{ $ctrl.feedback.message }}
35 |
36 |
37 |
38 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/sass/buttons.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .br-button
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .br-button {
5 | display: inline-block;
6 | cursor: pointer;
7 | font-size: 1em;
8 | padding: 4px 8px;
9 | border: 1px solid var(--brand-primary-30);
10 | border-radius: 2px;
11 | margin: 0 4px 0 0;
12 | color: var(--brand-primary-30);
13 | font-weight: 400;
14 | background-color: transparent;
15 | transition: all 0.25s ease;
16 | }
17 |
18 | .br-button--full {
19 | width: 100%;
20 | }
21 |
22 | .br-button:hover {
23 | color: var(--white);
24 | background: var(--brand-primary-30);
25 | text-decoration: none;
26 | }
27 |
28 | .br-button.warning {
29 | border-color: var(--gray-20);
30 | color: var(--gray-20);
31 | }
32 |
33 | .br-button.warning:hover {
34 | background: var(--gray-30);
35 | color: var(--white);
36 | }
37 |
38 | .br-button.destructive {
39 | border-color: var(--danger-base);
40 | color: var(--danger-base);
41 | }
42 |
43 | .br-button.destructive:hover {
44 | background: var(--danger-base);
45 | color: var(--white);
46 | }
47 |
48 | .br-button.btn-disable {
49 | cursor: not-allowed;
50 | pointer-events: none;
51 | color: #c0c0c0;
52 | background-color: #ddd;
53 | }
54 |
55 | ////////////////////////////////////////////////////////////////////////////////
56 | // .btn-alt
57 | ////////////////////////////////////////////////////////////////////////////////
58 | .btn-alt {
59 | background-color: var(--brand-default);
60 | color: var(--white);
61 | text-transform: none;
62 | }
63 |
64 | .btn-alt:hover {
65 | background-color: transparent;
66 | color: var(--brand-default);
67 | }
68 |
69 | ////////////////////////////////////////////////////////////////////////////////
70 | // .btn-alt
71 | ////////////////////////////////////////////////////////////////////////////////
72 | .btn-alt {
73 | background-color: var(--brand-default);
74 | color: var(--white);
75 | text-transform: none;
76 | }
77 |
78 | .btn-alt:hover {
79 | background-color: transparent;
80 | color: var(--brand-default);
81 | }
--------------------------------------------------------------------------------
/server_app/model/handler.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../app");
3 |
4 | jest.mock("./service");
5 | jest.mock('jsonwebtoken');
6 | process.env.SECRET_TOKEN = 'mockSecretToken';
7 |
8 | const mockModelService = require("./service");
9 |
10 | jest.mock('../helpers/config', () => ({
11 | SecretToken: 'mockSecretToken',
12 | }));
13 | jest.mock('jsonwebtoken', () => ({
14 | ...jest.requireActual('jsonwebtoken'), // import and keep all other methods unchanged
15 | sign: jest.fn(() => 'mockedToken'),
16 | verify: jest.fn((token, secret, cb) => {
17 | cb(null, { id: 'global-user-id' });
18 | }),
19 | }));
20 |
21 |
22 | afterEach(() => {
23 | jest.clearAllMocks();
24 | });
25 |
26 |
27 | describe("Test save /models", () => {
28 | test("It should send 422 when validation fails", async () => {
29 | const response = await request(app)
30 | .post("/models")
31 | .set("brx-access-token", 'mockToken')
32 | .send({
33 | name: "Teste",
34 | type: "conceptual",
35 | model: { id: "1234" },
36 | });
37 | expect(response.statusCode).toBe(422);
38 | });
39 |
40 | test("It should send 200 when user model is saved", async () => {
41 | const response = await request(app)
42 | .post("/models")
43 | .set("brx-access-token", 'mockToken')
44 | .send({
45 | name: "Teste",
46 | type: "conceptual",
47 | model: { id: "1234" },
48 | user: "12345",
49 | });
50 | mockModelService.save.mockResolvedValue({
51 | name: "Teste",
52 | type: "conceptual",
53 | model: { id: "1234" },
54 | user: "12345",
55 | });
56 | expect(response.statusCode).toBe(200);
57 | expect(mockModelService.save).toHaveBeenCalled();
58 | });
59 | });
60 |
61 | describe("Test list all /models", () => {
62 |
63 | beforeEach(() => {
64 | jest.resetModules();
65 | });
66 |
67 | test("It should send 200 user exists", async () => {
68 | const response = await request(app)
69 | .get("/models")
70 | .set("brx-access-token", 'mockToken')
71 | .send([]);
72 | mockModelService.listAll.mockResolvedValue([]);
73 |
74 | expect(response.statusCode).toBe(200);
75 | expect(mockModelService.listAll).toHaveBeenCalled();
76 | });
77 | });
--------------------------------------------------------------------------------
/app/angular/recovery/reset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
9 |
10 |
11 | {{ 'Redefine password' | translate }}
12 |
13 |
14 | {{ $ctrl.feedback.message }}
15 |
16 |
49 |
55 |
56 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 | const TerserPlugin = require("terser-webpack-plugin");
5 | const Dotenv = require("dotenv-webpack");
6 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin")
7 |
8 | module.exports = {
9 | mode: process.env.NODE_ENV,
10 | context: `${__dirname}/app`,
11 | entry: "./angular/index.js",
12 | output: {
13 | path: `${__dirname}/app/dist`,
14 | filename: "[name].js",
15 | },
16 | resolve: {
17 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
18 | plugins: [
19 | new TsconfigPathsPlugin({
20 | configFile: path.resolve(__dirname, "app/react/tsconfig.json"),
21 | }),
22 | ],
23 | },
24 | devtool: process.env.NODE_ENV === "production" ? false : "inline-source-map",
25 | devServer: {
26 | static: path.join(__dirname, "app"),
27 | compress: true,
28 | port: 9000,
29 | },
30 | plugins: [
31 | new Dotenv({ systemvars: true }),
32 | new HtmlWebpackPlugin({
33 | template: "index.html",
34 | favicon: `${__dirname}/app/img/brmw-logo.svg`,
35 | }),
36 | new MiniCssExtractPlugin({
37 | filename: `bundle.css`,
38 | }),
39 | ],
40 | optimization: {
41 | minimizer: [
42 | new TerserPlugin({
43 | parallel: true,
44 | }),
45 | ],
46 | splitChunks: {
47 | chunks: "all",
48 | },
49 | },
50 | module: {
51 | rules: [
52 | {
53 | test: /\.tsx?$/,
54 | use: "ts-loader",
55 | exclude: /node_modules/,
56 | },
57 | {
58 | test: /\.(js|jsx)$/,
59 | exclude: /node_modules/,
60 | use: ["babel-loader"],
61 | },
62 | {
63 | test: /\.html$/i,
64 | loader: "html-loader",
65 | },
66 | {
67 | test: /\.(png|jpe?g|gif|svg)$/i,
68 | type: "asset/resource",
69 | },
70 | {
71 | test: /\.(sa|sc|c)ss$/,
72 | use: [
73 | {
74 | loader: "style-loader",
75 | },
76 | {
77 | loader: "css-loader",
78 | },
79 | {
80 | loader: "postcss-loader",
81 | },
82 | {
83 | loader: "sass-loader",
84 | options: {
85 | implementation: require("sass"),
86 | },
87 | },
88 | ],
89 | },
90 | {
91 | test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
92 | type: "asset/resource",
93 | },
94 | ],
95 | },
96 | };
97 |
--------------------------------------------------------------------------------
/app/angular/components/noteEditor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/app/angular/components/keyboardController.js:
--------------------------------------------------------------------------------
1 | import hotkeys from 'hotkeys-js';
2 |
3 | export const types = {
4 | SAVE: 'save',
5 | UNDO: 'undo',
6 | REDO: 'redo',
7 | ZOOM_IN: 'zoomIn',
8 | ZOOM_OUT: 'zoomOut',
9 | ZOOM_NONE: 'zoomNone',
10 | ESC: 'esc',
11 | DELETE: 'delete',
12 | COPY: 'copy',
13 | PASTE: 'paste',
14 | }
15 |
16 | export default class KeyboardController {
17 | constructor(document) {
18 | this.document = document;
19 | this.spacePressed = false;
20 | this.shortcutHandlers = new Map();
21 | this.#registerSpaceEvents();
22 | this.#registerShortcutEvents();
23 | }
24 |
25 | #registerSpaceEvents() {
26 | this.document.on('keydown', (keyboardEvent) => {
27 | if (keyboardEvent.code === "Space" && keyboardEvent.originalEvent.target == document.body) {
28 | this.spacePressed = true;
29 | keyboardEvent.preventDefault();
30 | }
31 | });
32 |
33 | this.document.on('keyup', (keyboardEvent) => {
34 | if (keyboardEvent.code === "Space") {
35 | this.spacePressed = false;
36 | }
37 | });
38 | }
39 |
40 | #registerShortcutEvents() {
41 | hotkeys.unbind();
42 |
43 | this.hotkeyConfigs.forEach(({ key, eventName }) => {
44 | hotkeys(key, () => {
45 | this.shortcutHandlers.get(eventName).forEach(action => action());
46 | return false;
47 | });
48 | })
49 | }
50 |
51 | get isSpacePressed() {
52 | return this.spacePressed;
53 | }
54 |
55 | get hotkeyConfigs() {
56 | return [
57 | { key: 'command+s, ctrl+s', eventName: types.SAVE },
58 | { key: 'command+z, ctrl+z', eventName: types.UNDO },
59 | { key: 'command+shift+z, ctrl+shift+z', eventName: types.REDO },
60 | { key: 'backspace', eventName: types.DELETE },
61 | { key: 'command+c, ctrl+c', eventName: types.COPY },
62 | { key: 'command+v, ctrl+v', eventName: types.PASTE },
63 | { key: 'z+=', eventName: types.ZOOM_IN },
64 | { key: 'z+-', eventName: types.ZOOM_OUT },
65 | { key: 'z+0', eventName: types.ZOOM_NONE },
66 | { key: 'esc', eventName: types.ESC },
67 | ];
68 | }
69 |
70 | registerHandler(eventName, action) {
71 | const currentActions = this.shortcutHandlers.has(eventName) ? this.shortcutHandlers.get(eventName) : [];
72 | this.shortcutHandlers.set(eventName, [action, ...currentActions])
73 | }
74 |
75 | unbindAll() {
76 | this.document.off("keydown");
77 | this.document.off("keyup");
78 | hotkeys.unbind();
79 | }
80 | };
--------------------------------------------------------------------------------
/app/angular/publicview/publicview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
42 |
43 |
--------------------------------------------------------------------------------
/app/angular/preferences/preferences.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./preferences.html";
3 | import confirmationModal from "../components/confirmationModal";
4 |
5 | const preferencesController = function(AuthService, $state, $filter, $uibModal, $timeout){
6 | const ctrl = this;
7 | ctrl.loading = false;
8 | ctrl.feedback = {
9 | message: "",
10 | showing: false
11 | }
12 |
13 | ctrl.showFeedback = (show, newMessage) => {
14 | $timeout(() => {
15 | ctrl.feedback.showing = show;
16 | ctrl.feedback.message = $filter('translate')(newMessage);
17 | });
18 | }
19 |
20 | const showLoading = (loading) => {
21 | ctrl.loading = loading;
22 | };
23 |
24 | const handleDeleteAccountError = (error) => {
25 | if(error.status == 400) {
26 | const errorMessage = $filter('translate')("Delete account model error");
27 | ctrl.showFeedback(true, errorMessage);
28 | } else {
29 | const errorMessage = $filter('translate')("Delete account default error");
30 | ctrl.showFeedback(true, errorMessage);
31 | }
32 | }
33 |
34 | ctrl.dropdownOptions = [
35 | { name: $filter('translate')("Preferences"), type: 'preferences' },
36 | { name: $filter('translate')("Logout"), type: 'logout' }
37 | ];
38 |
39 | ctrl.deleteAccount = () => {
40 | const modalInstance = $uibModal.open({
41 | animation: true,
42 | component: "confirmationModal",
43 | resolve: {
44 | modalData: () => ({
45 | title: $filter('translate')("Delete account"),
46 | content: $filter('translate')("Are you sure you want to delete your account?"),
47 | cancelLabel: $filter('translate')("Cancel"),
48 | confirmLabel: $filter('translate')("Delete"),
49 | }),
50 | },
51 | });
52 |
53 | modalInstance.result.then(() => {
54 | showLoading(true);
55 | AuthService.deleteAccount().then(() => {
56 | AuthService.logout();
57 | $state.go("login");
58 | }).catch(handleDeleteAccountError)
59 | .finally(() => {
60 | showLoading(false);
61 | });
62 | });
63 | }
64 |
65 | ctrl.menuOptionSelected = (option) => {
66 | switch(option.type) {
67 | case "logout": ctrl.doLogout()
68 | break;
69 | case "preferences": $state.go("preferences");
70 | break;
71 | }
72 | }
73 |
74 | ctrl.doLogout = () => {
75 | AuthService.logout();
76 | $state.go("login");
77 | }
78 | }
79 |
80 | export default angular
81 | .module("app.preferences", [confirmationModal])
82 | .component("preferences", {
83 | template,
84 | controller: preferencesController,
85 | }).name;
--------------------------------------------------------------------------------
/cypress/e2e/login.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | describe("Login", () => {
4 | const user = Cypress.env("user");
5 | const password = Cypress.env("password");
6 |
7 | it("logs in and out successfully", () => {
8 | cy.intercept("GET", "/models?userId=*").as("getUserModels");
9 |
10 | cy.loginViaGui(user, password);
11 |
12 | cy.wait("@getUserModels");
13 | cy.url().should("be.equal", `${Cypress.config("baseUrl")}/#!/main`);
14 | cy.contains("h2", "Models").should("be.visible");
15 |
16 | cy.get("[data-cy='gear-button']").click();
17 | cy.contains(".dropdown-menu li", "Logout").click();
18 |
19 | cy.url().should("be.equal", `${Cypress.config("baseUrl")}/#!/`);
20 | cy.get(".login-form").should("be.visible");
21 | });
22 |
23 | it("switches languages", () => {
24 | cy.visit("/");
25 |
26 | cy.get('button[type="submit"]')
27 | .as("submitButton")
28 | .should("contain", "Login")
29 | .and("be.visible");
30 |
31 | cy.get("[data-cy='lang-pt-br']").click();
32 |
33 | cy.get("@submitButton").should("contain", "Entrar").and("be.visible");
34 |
35 | cy.get("[data-cy='lang-en-us']").click();
36 |
37 | cy.get("@submitButton").should("contain", "Login").and("be.visible");
38 | });
39 |
40 | context("Form validations", () => {
41 | it("alerts on invalid user email", () => {
42 | cy.loginViaGui("invalid#user.com", password);
43 |
44 | cy.contains(".alert-danger", "Fill the fields in red").should(
45 | "be.visible"
46 | );
47 | cy.get("#userEmail").should("have.class", "error");
48 | cy.get(".error").should("have.length", 1).and("be.visible");
49 | });
50 |
51 | it("alerts when clicking ENTER without filling the form", () => {
52 | cy.visit("/");
53 | cy.contains("button", "Login").click();
54 |
55 | cy.contains(".alert-danger", "Fill the fields in red").should(
56 | "be.visible"
57 | );
58 | cy.get("#userEmail").should("have.class", "error");
59 | cy.get("#userPassword").should("have.class", "error");
60 | cy.get(".error").should("have.length", 2).and("be.visible");
61 | });
62 |
63 | it("alerts on invalid user", () => {
64 | cy.loginViaGui("invalid@user.com", password);
65 |
66 | cy.contains(".alert-danger", "Incorrect login or password").should(
67 | "be.visible"
68 | );
69 | });
70 |
71 | it("alerts on invalid password", () => {
72 | cy.loginViaGui(user, "invalid-pwd");
73 |
74 | cy.contains(".alert-danger", "Incorrect login or password").should(
75 | "be.visible"
76 | );
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/app/angular/signup/signup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
9 |
10 |
11 | {{ 'Create account' | translate }}
12 |
13 |
14 | {{ $ctrl.feedback.message }}
15 |
16 |
61 |
67 |
68 |
--------------------------------------------------------------------------------
/app/angular/conceptual/factory.test.js:
--------------------------------------------------------------------------------
1 | import Factory from "./factory";
2 |
3 | import * as joint from "jointjs";
4 |
5 | import shapes from "../../joint/shapes";
6 | joint.shapes.erd = shapes;
7 |
8 | describe('factory', () => {
9 |
10 | const factory = new Factory(joint.shapes);
11 | const position = { x: 0, y: 1 };
12 |
13 | test("It should create an Entity", async () => {
14 | const entity = factory.createEntity( { "position": position });
15 |
16 | expect(entity.attributes.type).toBe("erd.Entity");
17 | expect(entity.attributes.supertype).toBe("Entity");
18 | expect(entity.attributes.isExtended).toBeFalsy();
19 | expect(entity.attributes.autorelationship).toBeFalsy();
20 | expect(entity.attributes.position).toMatchObject(position);
21 | });
22 |
23 | test("It should create a Relationship", async () => {
24 | const entity = factory.createRelationship({ "position": position });
25 |
26 | expect(entity.attributes.type).toBe("erd.Relationship");
27 | expect(entity.attributes.supertype).toBe("Relationship");
28 | expect(entity.attributes.autorelationship).toBeFalsy();
29 | expect(entity.attributes.position).toMatchObject(position);
30 | });
31 |
32 | test("It should create an Extension", async () => {
33 | const entity = factory.createIsa({ "position": position });
34 |
35 | expect(entity.attributes.type).toBe("erd.ISA");
36 | expect(entity.attributes.supertype).toBe("Inheritance");
37 | expect(entity.attributes.parentId).toBeNull();
38 | expect(entity.attributes.position).toMatchObject(position);
39 | });
40 |
41 | test("It should create an Attribute", async () => {
42 | const entity = factory.createAttribute({ "position": position });
43 |
44 | expect(entity.attributes.type).toBe("erd.Attribute");
45 | expect(entity.attributes.supertype).toBe("Attribute");
46 | expect(entity.attributes.multivalued).toBeFalsy();
47 | expect(entity.attributes.composed).toBeFalsy();
48 | });
49 |
50 | test("It should create a Key", async () => {
51 | const entity = factory.createKey({ "position": position });
52 |
53 | expect(entity.attributes.type).toBe("erd.Key");
54 | expect(entity.attributes.supertype).toBe("Key");
55 | expect(entity.attributes.multivalued).toBeFalsy();
56 | expect(entity.attributes.composed).toBeFalsy();
57 | });
58 |
59 | test("It should create a Link", async () => {
60 | const entity = factory.createLink();
61 |
62 | expect(entity.attributes.type).toBe("erd.Link");
63 | expect(entity.attributes.weak).toBeFalsy();
64 | });
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/app/angular/login/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
9 |
10 |
13 |
14 | {{ $ctrl.feedback.message }}
15 |
16 |
47 |
55 |
56 |
65 |
66 |
--------------------------------------------------------------------------------
/app/angular/service/authService.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import { Buffer } from 'buffer';
3 |
4 | const authService = function ($http, $cookies) {
5 | const service = {};
6 |
7 | service.login = function (credentials) {
8 | const body = {
9 | "username": service.encode(credentials.username),
10 | "password": service.encode(credentials.password)
11 | }
12 | return $http.post("/users/login", body).then((res) => {
13 | const user = res.data;
14 | const today = new Date();
15 | const expired = new Date(today);
16 | expired.setDate(today.getDate() + 1);
17 | $cookies.put("sessionId", user.sessionId, { expires: expired });
18 | $cookies.put("userId", user.userId, { expires: expired });
19 | $cookies.put("userName", user.userName, { expires: expired });
20 | $cookies.put("userToken", user.token, { expires: expired });
21 | return user;
22 | });
23 | };
24 |
25 | service.logout = function () {
26 | $cookies.remove("sessionId");
27 | $cookies.remove("userId");
28 | $cookies.remove("userName");
29 | $cookies.remove("userToken");
30 | };
31 |
32 | service.register = function (credentials) {
33 | const body = {
34 | "email": service.encode(credentials.email),
35 | "password": service.encode(credentials.password)
36 | }
37 | return $http.post("/users/create", body).then((res) => {});
38 | };
39 |
40 | service.isAuthenticated = function () {
41 | const userId = $cookies.get("userId");
42 | service.loggeduser = userId;
43 | service.loggeduserName = $cookies.get("userName");
44 | service.token = $cookies.get("userToken");
45 | return !!userId;
46 | };
47 |
48 | service.recovery = (email) => {
49 | return $http.post("/users/recovery", { email });
50 | };
51 |
52 | service.validateRecovery = (mail, code) => {
53 | return $http.get("/users/recovery/validate", {
54 | params: { mail, code },
55 | });
56 | };
57 |
58 | service.resetPassword = (mail, code, newPassword) => {
59 | const body = {
60 | "mail": service.encode(mail),
61 | "newPassword": service.encode(newPassword),
62 | "code": code
63 | }
64 | return $http.post("/users/reset", body);
65 | };
66 |
67 | service.deleteAccount = () => {
68 | return $http({
69 | method: 'delete',
70 | url: '/users/delete',
71 | data: {
72 | "userId": service.loggeduser
73 | },
74 | headers: {
75 | 'Content-type': 'application/json;charset=utf-8'
76 | }
77 | });
78 | };
79 |
80 | service.encode = (data) => {
81 | return Buffer.from(data).toString('base64');
82 | };
83 |
84 | return service;
85 | };
86 |
87 | export default angular
88 | .module("app.authService", [])
89 | .factory("AuthService", authService).name;
90 |
--------------------------------------------------------------------------------
/app/img/brmw-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/angular/view/view.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
85 |
--------------------------------------------------------------------------------
/app/angular/view/view.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import { comparasionOperators } from "../service/queryExpressionService";
3 | import template from "./view.html";
4 |
5 | const app = angular.module("app.view", []);
6 |
7 | const Controller = function (LogicService, $uibModal) {
8 | const $ctrl = this;
9 |
10 | const updateViewInfo = (view) => {
11 | const allTables = LogicService.loadTables();
12 | $ctrl.tables = view.tables;
13 | $ctrl.queryConditions = view.queryConditions;
14 | allTables.forEach(table => {
15 | if ($ctrl.tables.some(({ name }) => table.name === name)) return;
16 | $ctrl.tables.push(table);
17 | });
18 | }
19 |
20 | $ctrl.$onInit = () => {
21 | updateViewInfo($ctrl.view);
22 | };
23 |
24 | $ctrl.queryExpressionModal = () => {
25 | const modalInstance = $uibModal.open({
26 | animation: true,
27 | template: '',
28 | controller: function () {
29 | const ctrl = this;
30 | ctrl.tables = $ctrl.tables.filter(({ selected }) => selected);
31 | ctrl.queryConditions = $ctrl.view.queryConditions;
32 | },
33 | controllerAs: '$ctrl',
34 | });
35 | modalInstance.result.then(({ conditions, joins }) => {
36 | $ctrl.view.queryConditions = {
37 | joins: joins.filter(join => join.submitted),
38 | values: conditions,
39 | text: `${conditions.map(({ columnName, type, comparativeValue, comparativeValue2, logicalOperator }, index) =>
40 | `${columnName} ${comparasionOperators[type](comparativeValue, comparativeValue2)} ${conditions.length - 1 === index ? '' : logicalOperator}`).join(" ")}`
41 | }
42 | });
43 | };
44 |
45 | $ctrl.changeName = () => {
46 | if ($ctrl.view.name) {
47 | LogicService.editName($ctrl.view.name);
48 | }
49 | }
50 |
51 | $ctrl.$onChanges = (changes) => {
52 | if (changes.view != null && changes.view.currentValue != null) {
53 | updateViewInfo({ ...changes.view.currentValue });
54 | }
55 | }
56 |
57 | $ctrl.toggleForm = () => {
58 | $ctrl.formVisible = !$ctrl.formVisible;
59 | };
60 |
61 | $ctrl.cancel = () => {
62 | $ctrl.dismiss();
63 | };
64 |
65 | $ctrl.save = () => {
66 | let columns = [];
67 | const filteredTables = $ctrl.tables.filter(({ selected }) => selected);
68 | filteredTables.forEach(table => {
69 | table.columns.forEach(column => {
70 | if (column.selected) columns.push(column);
71 | });
72 | });
73 | const view = {
74 | ...$ctrl.view,
75 | basedIn: filteredTables,
76 | columns,
77 | }
78 | LogicService.save(view);
79 | }
80 | };
81 |
82 | export default app.component("view", {
83 | template: template,
84 | bindings: {
85 | view: "<",
86 | isEdit: "<",
87 | dismiss: "<"
88 | },
89 | controller: Controller,
90 | }).name;
91 |
--------------------------------------------------------------------------------
/cypress/e2e/models.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { faker } from '@faker-js/faker/locale/en';
4 |
5 | describe("Models view", () => {
6 | beforeEach(() => {
7 | cy.intercept("GET", "/models?userId=*").as("getUserModels");
8 | cy.loginViaGui();
9 | cy.wait("@getUserModels").then((userModels) => {
10 | const userId = userModels.request.url.match(/userId=([^&]*)/)[1];
11 |
12 | cy.cleanUpUserModels(userModels);
13 | cy.createModelViaApi("conceptual", userId);
14 | });
15 | cy.reload();
16 | cy.wait("@getUserModels");
17 | });
18 |
19 | it("edits a model title", () => {
20 | const updatedModelName = faker.lorem.word();
21 |
22 | cy.get(".fa-pencil").click({ force: true });
23 | cy.get("#rename-model").type(updatedModelName);
24 | cy.contains("button", "Rename").click();
25 | cy.contains("tr.listLine", updatedModelName).should("be.visible");
26 | });
27 |
28 | // @TODO: figure out why model duplication isn't working.
29 | it.skip("duplicates a model", () => {
30 | cy.intercept("POST", "/models").as("postModel");
31 | cy.get(".fa-files-o").click({ force: true });
32 | cy.contains("button", "Save").click();
33 | cy.wait("@postModel").its("response.statusCode").should("be.equal", 200);
34 | cy.reload();
35 | cy.get("tr.listLine")
36 | .should("have.length", 2)
37 | .last()
38 | .should("contain", "(copy)");
39 | });
40 |
41 | it("deletes a model", () => {
42 | cy.intercept("DELETE", "/models/**").as("deleteModels");
43 | cy.get(".fa-trash").click({ force: true });
44 | cy.contains("button", "Delete").click();
45 | cy.wait("@deleteModels").its("response.statusCode").should("be.equal", 200);
46 | cy.get("table tbody tr.listLine").should("not.exist");
47 | });
48 |
49 | it("opens and closes the modals", () => {
50 | // Opens the rename model modal
51 | cy.get(".fa-pencil").click({ force: true });
52 | // Make sure the modal is visible and closes it
53 | cy.get(".modal-dialog")
54 | .as("modal")
55 | .should("be.visible")
56 | .find("button:contains(Cancel)")
57 | .click();
58 | // Make sure the modal doesn't exist in the DOM anymore
59 | cy.get("@modal").should("not.exist");
60 | // Opens the duplicate model modal
61 | cy.get(".fa-files-o").click({ force: true });
62 | // Make sure the modal is visible and closes it
63 | cy.get("@modal")
64 | .should("be.visible")
65 | .find("button:contains(Cancel)")
66 | .click();
67 | // Make sure the modal doesn't exist in the DOM anymore
68 | cy.get("@modal").should("not.exist");
69 | // Opens the delete model modal
70 | cy.get(".fa-trash").click({ force: true });
71 | // Make sure the modal is visible and closes it
72 | cy.get("@modal")
73 | .should("be.visible")
74 | .find("button:contains(Cancel)")
75 | .click();
76 | // Make sure the modal doesn't exist in the DOM anymore
77 | cy.get("@modal").should("not.exist");
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/server_app/model/validator.test.js:
--------------------------------------------------------------------------------
1 | const modelValidator = require("./validator");
2 |
3 | describe("validateSaveModelParams test", () => {
4 | test("should fail when name is missing", () => {
5 | const validation = modelValidator.validateSaveParams({});
6 | expect(validation.valid).toBeFalsy();
7 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_NAME);
8 | });
9 |
10 | test("should fail when name is empty", () => {
11 | const validation = modelValidator.validateSaveParams({ name: "" });
12 | expect(validation.valid).toBeFalsy();
13 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_NAME);
14 | });
15 |
16 | test("should fail when name has a wrong type", () => {
17 | const validation = modelValidator.validateSaveParams({ name: 5 });
18 | expect(validation.valid).toBeFalsy();
19 | expect(validation.message).toBe(
20 | modelValidator.getMessages().WRONG_NAME_TYPE
21 | );
22 | });
23 |
24 | test("should fail when type is missing", () => {
25 | const validation = modelValidator.validateSaveParams({ name: "Teste" });
26 | expect(validation.valid).toBeFalsy();
27 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_TYPE);
28 | });
29 |
30 | test("should fail when type is empty", () => {
31 | const validation = modelValidator.validateSaveParams({
32 | name: "Teste",
33 | type: "",
34 | });
35 | expect(validation.valid).toBeFalsy();
36 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_TYPE);
37 | });
38 |
39 | test("should fail when model does not have a type", () => {
40 | const validation = modelValidator.validateSaveParams({
41 | name: "Teste",
42 | type: 5,
43 | });
44 | expect(validation.valid).toBeFalsy();
45 | expect(validation.message).toBe(
46 | modelValidator.getMessages().WRONG_TYPE_TYPE
47 | );
48 | });
49 |
50 | test("should fail when user is missing", () => {
51 | const validation = modelValidator.validateSaveParams({
52 | name: "Teste",
53 | type: "conceltual",
54 | model: { id: "1234" },
55 | });
56 | expect(validation.valid).toBeFalsy();
57 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_USER);
58 | });
59 |
60 | test("should fail when user is 0", () => {
61 | const validation = modelValidator.validateSaveParams({
62 | name: "Teste",
63 | type: "conceltual",
64 | model: { id: "1234" },
65 | userId: "",
66 | });
67 | expect(validation.valid).toBeFalsy();
68 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_USER);
69 | });
70 |
71 | test("should fail when userID has a wrong type", () => {
72 | const validation = modelValidator.validateSaveParams({
73 | name: "Teste",
74 | type: "conceptual",
75 | model: { id: "1234" },
76 | userId: 5,
77 | });
78 | expect(validation.valid).toBeFalsy();
79 | expect(validation.message).toBe(
80 | modelValidator.getMessages().WRONG_USER_TYPE
81 | );
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/app/sass/colors.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | ///////////////////////////////////////////////////////////////////////////////
3 | // Brand primary colors
4 | ///////////////////////////////////////////////////////////////////////////////
5 | --brand-primary-50: hsl(170, 100%, 50%);
6 | --brand-primary-40: hsl(170, 100%, 40%);
7 | --brand-primary-30: hsl(170, 100%, 30%);
8 | --brand-primary-20: hsl(170, 100%, 20%);
9 | --brand-primary-10: hsl(170, 100%, 10%);
10 |
11 | --brand-default: var(--brand-primary-20);
12 |
13 | ///////////////////////////////////////////////////////////////////////////////
14 | // Gray scale colors
15 | ///////////////////////////////////////////////////////////////////////////////
16 | --gray-95: hsl(0, 0%, 95%);
17 | --gray-90: hsl(0, 0%, 90%);
18 | --gray-80: hsl(0, 0%, 80%);
19 | --gray-70: hsl(0, 0%, 70%);
20 | --gray-50: hsl(0, 0%, 50%);
21 | --gray-30: hsl(0, 0%, 30%);
22 | --gray-20: hsl(0, 0%, 20%);
23 | --gray-10: hsl(0, 0%, 10%);
24 |
25 | ///////////////////////////////////////////////////////////////////////////////
26 | // Black & white pure values
27 | ///////////////////////////////////////////////////////////////////////////////
28 | --white: hsl(0, 0%, 100%);
29 | --black: hsl(0, 0%, 0%);
30 |
31 | ///////////////////////////////////////////////////////////////////////////////
32 | // Accent color
33 | ///////////////////////////////////////////////////////////////////////////////
34 | --accent-light: hsl(60, 70%, 90%);
35 | --accent-base: hsl(60, 70%, 50%);
36 | --accent-dark: hsl(60, 70%, 30%);
37 |
38 | ///////////////////////////////////////////////////////////////////////////////
39 | // Border colors
40 | ///////////////////////////////////////////////////////////////////////////////
41 | --border-light: var(--gray-90);
42 | --border-default: var(--gray-70);
43 | --border-dark: var(--gray-30);
44 |
45 | ///////////////////////////////////////////////////////////////////////////////
46 | // Meaning related colors
47 | ///////////////////////////////////////////////////////////////////////////////
48 | --danger-light: hsl(340, 100%, 60%);
49 | --danger-base: hsl(340, 100%, 40%);
50 | --danger-dark: hsl(340, 100%, 20%);
51 |
52 | --highlight-light: hsl(45, 86%, 80%);
53 | --highlight-base: hsl(45, 86%, 60%);
54 | --highlight-dark: hsl(45, 86%, 30%);
55 |
56 | ///////////////////////////////////////////////////////////////////////////////
57 | // Theme: Gray
58 | ///////////////////////////////////////////////////////////////////////////////
59 | --theme-gray: var(--gray-30);
60 |
61 | ///////////////////////////////////////////////////////////////////////////////
62 | // Theme: Green
63 | ///////////////////////////////////////////////////////////////////////////////
64 | --theme-green: var(--brand-primary-30);
65 |
66 | ///////////////////////////////////////////////////////////////////////////////
67 | // Theme: Purple
68 | ///////////////////////////////////////////////////////////////////////////////
69 | --theme-purple: hsl(24, 80% 60%);
70 | }
--------------------------------------------------------------------------------
/app/angular/recovery/reset.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./reset.html";
3 | import AuthService from "../service/authService"
4 |
5 | const controller = function (AuthService, $stateParams, $timeout, $state, $filter) {
6 | const ctrl = this;
7 | ctrl.submitted = false;
8 | ctrl.mail = "";
9 | ctrl.validation = {
10 | validCode: false,
11 | }
12 | ctrl.feedback = {
13 | message: "",
14 | showing: false,
15 | type: "error"
16 | };
17 |
18 | const setValidation = (valid) => {
19 | $timeout(() => {
20 | ctrl.validation.validCode = valid;
21 | }, 100);
22 | };
23 |
24 | const setLoading = (loading) => {
25 | $timeout(() => {
26 | ctrl.loading = loading;
27 | }, 100);
28 | };
29 |
30 | const showFeedback = (newMessage, type = "error") => {
31 | $timeout(() => {
32 | ctrl.feedback.message = $filter('translate')(newMessage);
33 | ctrl.feedback.showing = true;
34 | ctrl.feedback.type = type;
35 | }, 100);
36 | };
37 |
38 | const handleError = (error) => {
39 | console.log(error);
40 | ctrl.loading = false;
41 | showFeedback("This email is not registered yet.");
42 | };
43 |
44 | const handleSuccess = () => {
45 | showFeedback("The new password was successfully saved.", "success");
46 | $timeout(() => {
47 | setLoading(false);
48 | $state.go("login");
49 | }, 2000);
50 | };
51 |
52 | const doReset = (newPassword) => {
53 | ctrl.loading = true;
54 | const code = $stateParams.code;
55 | const mail = $stateParams.mail;
56 | AuthService.resetPassword(mail, code, newPassword)
57 | .then(handleSuccess)
58 | .catch(handleError);
59 | };
60 |
61 | ctrl.submitForm = (validForm, newPassword, repeatedPassword) => {
62 | ctrl.submitted = true;
63 | ctrl.feedback.showing = false;
64 | if (validForm) {
65 | if (newPassword != repeatedPassword) {
66 | showFeedback("The password must be the same in both fields.");
67 | } else if (newPassword.length < 6) {
68 | showFeedback("The password must contain more than 6 digits.");
69 | } else {
70 | doReset(newPassword)
71 | }
72 | } else {
73 | showFeedback("Fill the fields in red");
74 | }
75 | };
76 |
77 | ctrl.$onInit = () => {
78 | setLoading(true);
79 | const code = $stateParams.code;
80 | const mail = $stateParams.mail;
81 | setLoading(true);
82 | AuthService.validateRecovery(mail, code)
83 | .then((response) => {
84 | setLoading(false);
85 | if (response.data.valid) {
86 | setValidation(true);
87 | } else {
88 | showFeedback("Password recovery code is invalid, request password recovery again!");
89 | }
90 | }).catch((error) => {
91 | setLoading(false);
92 | showFeedback("Password recovery code is invalid, request password recovery again!");
93 | });
94 | }
95 |
96 | };
97 |
98 | export default angular
99 | .module("app.reset", [AuthService])
100 | .component("resetPassword", {
101 | template,
102 | controller,
103 | }).name;
--------------------------------------------------------------------------------
/app/angular/service/modelAPI.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 |
3 | const authService = ($http) => {
4 | const _saveModel = function (model) {
5 | return $http.post("/models", model).then(function (newModel) {
6 | return newModel.data;
7 | });
8 | };
9 |
10 | const _getAllModels = function (_userId) {
11 | return $http
12 | .get("/models", {
13 | params: { userId: _userId },
14 | })
15 | .then(function (res) {
16 | return res;
17 | });
18 | };
19 |
20 | const _updateModel = function (model) {
21 | return $http.put(`/models/${model.id}`, model).then(function(resp) {
22 | return resp;
23 | });
24 | };
25 |
26 | const _getModel = function (_modelId, _userId) {
27 | return $http
28 | .get("/models/:modelId", {
29 | params: { userId: _userId, modelId: _modelId },
30 | })
31 | .then(function (resp) {
32 | return resp;
33 | });
34 | };
35 |
36 | const _deleteModel = function (_modelId) {
37 | return $http
38 | .delete("/models/:modelId", {
39 | params: { modelId: _modelId },
40 | })
41 | .then(function (resp) {
42 | return resp;
43 | });
44 | };
45 |
46 | const _renameModel = function (_modelId, newName) {
47 | return $http
48 | .put(`/models/${_modelId}/rename`, { name: newName })
49 | .then(function (resp) {
50 | return resp;
51 | });
52 | };
53 |
54 | const _loadShareOptions = function (_modelId) {
55 | return $http
56 | .get(`/models/${_modelId}/share/options`)
57 | .then(function (resp) {
58 | return resp;
59 | });
60 | };
61 |
62 | const _toggleShare = function (modelId, active, importAllowed) {
63 | return $http
64 | .post(`/models/share`, {"modelId": modelId, "active": active, "importAllowed": importAllowed})
65 | .then(function (resp) {
66 | return resp;
67 | });
68 | };
69 |
70 | const _getSharedModel = function (shareId) {
71 | return $http
72 | .get(`/models/share/${shareId}`)
73 | .then(function (resp) {
74 | return resp;
75 | });
76 | };
77 |
78 | const _importModel = function (shareId, userId) {
79 | return $http
80 | .post(`/models/import`, {shareId, userId})
81 | .then(function (resp) {
82 | return resp;
83 | });
84 | };
85 |
86 | const _duplicate= function (modelId, userId, newName) {
87 | return $http
88 | .post(`/models/${modelId}/duplicate`, {"userId": userId, "newName": newName})
89 | .then(function (resp) {
90 | return resp;
91 | });
92 | };
93 |
94 | return {
95 | saveModel: _saveModel,
96 | getAllModels: _getAllModels,
97 | getModel: _getModel,
98 | updateModel: _updateModel,
99 | deleteModel: _deleteModel,
100 | renameModel: _renameModel,
101 | loadShareOptions: _loadShareOptions,
102 | toggleShare: _toggleShare,
103 | getSharedModel: _getSharedModel,
104 | importModel: _importModel,
105 | duplicate: _duplicate
106 | };
107 | };
108 |
109 | export default angular
110 | .module("app.modelAPI", [])
111 | .factory("ModelAPI", authService).name;
112 |
--------------------------------------------------------------------------------
/app/sass/elementActions.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // .halo
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .element-action {
5 | position: absolute;
6 | pointer-events: none
7 | }
8 |
9 | ////////////////////////////////////////////////////////////////////////////////
10 | // .handles
11 | ////////////////////////////////////////////////////////////////////////////////
12 | .element-action .holder {
13 | display: flex;
14 | position: absolute;
15 | top: -60px;
16 | border-radius: 4px 4px 4px 0;
17 | border: 4px solid hsl(0, 0%, 91%);
18 | background-color: var(--gray-90);
19 | box-shadow: -1px 0 0 rgba(0,0,0, 0.1),
20 | 0 -1px 1px rgba(0,0,0, 0.3);
21 | }
22 |
23 | .element-action .holder:after {
24 | content: '';
25 | position: absolute;
26 | width: 0;
27 | height: 0;
28 | top: 100%;
29 | left: -4px;
30 | margin-top: 4px;
31 | border-top: 16px solid var(--gray-90);
32 | border-left: 0 solid transparent;
33 | border-right: 13px solid transparent;
34 | box-shadow: -1px 0 0 rgba(0,0,0, 0.2);
35 | }
36 |
37 | ////////////////////////////////////////////////////////////////////////////////
38 | // .halo .handle
39 | ////////////////////////////////////////////////////////////////////////////////
40 | .element-action .item.hidden {display: none;}
41 |
42 | .element-action .item {
43 | display: inline-block;
44 | position: relative;
45 | width: 36px;
46 | height: 36px;
47 | cursor: pointer;
48 | user-select: none;
49 | pointer-events: auto;
50 | background-size: 20px 20px;
51 | background-position: center;
52 | background-repeat: no-repeat;
53 | }
54 |
55 | .element-action .item:hover {
56 | background-color: var(--white);
57 | }
58 |
59 | ////////////////////////////////////////////////////////////////////////////////
60 | // .handle types
61 | ////////////////////////////////////////////////////////////////////////////////
62 | .item.clone,
63 | .item.link,
64 | .item.fork,
65 | .item.rotate {cursor: move;}
66 |
67 | .item.resize {cursor: se-resize;}
68 |
69 |
70 | ////////////////////////////////////////////////////////////////////////////////
71 | // select actions
72 | ////////////////////////////////////////////////////////////////////////////////
73 | .selection {
74 | position: absolute;
75 | border: 2px solid var(--brand-default);
76 | border-radius: 3px;
77 | overflow: visible;
78 | background-color: hsla(153, 43%, 42%, 0.2);
79 | }
80 |
81 | .selected .selection-wrapper {
82 | position: absolute;
83 | transform: scale(1.3);
84 | border: 2px solid var(--brand-default);
85 | border-radius: 3px;
86 | }
87 |
88 | .selection.selected {
89 | position: static;
90 | height: 0 !important;
91 | border: none;
92 | background-color: transparent;
93 | }
94 |
95 | .selection-box {
96 | position: absolute;
97 | border-radius: 2px;
98 | border: 1px dashed var(--brand-default);
99 | transform: scale(1.15);
100 | cursor: move;
101 | }
--------------------------------------------------------------------------------
/app/sass/public.scss:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // body (public)
3 | ////////////////////////////////////////////////////////////////////////////////
4 | .public {
5 | height: 100%;
6 | background-image: url(../img/wave01.svg),
7 | url(../img/wave02.svg),
8 | url(../img/wave03.svg);
9 | background-repeat: repeat-x;
10 | background-position: left bottom;
11 |
12 | }
13 | ////////////////////////////////////////////////////////////////////////////////
14 | // .login-form / .register-page
15 | ////////////////////////////////////////////////////////////////////////////////
16 | .login-page,
17 | .register-page,
18 | .recovery-page,
19 | .reset-page {
20 | position: relative;
21 | width: 375px;
22 | padding: 48px 24px 24px 24px;
23 | border: 1px solid var(--border-default);
24 | margin: 60px auto 0 auto;
25 | background-color: var(--white);
26 | box-shadow: 0 0 20px rgba(0,0,0, 0.1)
27 | }
28 |
29 | ////////////////////////////////////////////////////////////////////////////////
30 | // .login-form / .register-form
31 | ////////////////////////////////////////////////////////////////////////////////
32 | .login-form .br-button,
33 | .register-form .br-button,
34 | .recovery-form .br-button,
35 | .reset-form .br-button {
36 | padding: 10px 16px;
37 | font-size: 18px;
38 | }
39 |
40 | ////////////////////////////////////////////////////////////////////////////////
41 | // .public-logo
42 | ////////////////////////////////////////////////////////////////////////////////
43 | .public-logo {
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | margin-bottom: 24px;
48 | }
49 |
50 | .public-logo > img {
51 | margin-right: 8px;
52 | }
53 |
54 | .public-logo > h1 {
55 | color: var(--brand-default);
56 | margin: 0;
57 | font-weight: 700;
58 | }
59 |
60 | ////////////////////////////////////////////////////////////////////////////////
61 | // .account-options
62 | ////////////////////////////////////////////////////////////////////////////////
63 | .account-options {
64 | display: flex;
65 | flex-direction: column;
66 | justify-content: center;
67 | margin-top: 12px;
68 | text-align: center;
69 | font-size: 1.2em;
70 | }
71 |
72 | .br-button.register {
73 | padding: 10px 16px;
74 | }
75 |
76 | ////////////////////////////////////////////////////////////////////////////////
77 | // .language-selector
78 | ////////////////////////////////////////////////////////////////////////////////
79 | .language-selector {
80 | width: 375px;
81 | margin: 24px auto 0 auto;
82 | text-align: center;
83 | }
84 |
85 | .language-selector h2 {
86 | font-size: 1.2em;
87 | font-weight: 400;
88 | margin: 0 0 8px 0;
89 | }
90 |
91 | .language-selector h2 .fa {
92 | font-size: 2rem;
93 | }
94 |
95 | .language-list {
96 | display: flex;
97 | justify-content: center;
98 | padding: 0;
99 | list-style: none;
100 | }
101 |
102 | .language-list li::after {
103 | content: "|";
104 | display: inline-block;
105 | }
106 |
107 | .language-list li:last-child::after {
108 | display: none;
109 | }
110 |
111 | .language-list li a {
112 | margin: 4px;
113 | }
--------------------------------------------------------------------------------
/cypress/support/commands.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace Cypress {
4 | interface Chainable {
5 | /**
6 | * **Logs into the BR Modelo Wep App via the Graphical User Interface (GUI).**
7 | *
8 | * @param email string - The email of the user you want to log in with. Defaults to Cypress.env('user') if no value is provided.
9 | * @param password string - The password of the user you want to log in with. Defaults to Cypress.env('password') if no value is provided.
10 | *
11 | * @example cy.loginViaGui() // Logs into the app via GUI using the default email and password (defined as envs).
12 | * @example cy.loginViaGui('user@email.com', 'S3cRe7P@ssW0rd') // Logs into the app via GUI using the provided credentials.
13 | */
14 | loginViaGui(username?: string, password?: string)
15 |
16 | /**
17 | * **Deletes all models of the logged in user.**
18 | *
19 | * This command is used for cleaning things up before tests start so that they start on a clean state.
20 | *
21 | * @param userModels object - An object with the response of the request to '/models?userId=*'
22 | *
23 | * @example cy.cleanUpUserModels(userModelsResponseObject) // Cleans up all models for the logged in user
24 | */
25 | cleanUpUserModels(userModelsRespondeObject: object): Cypress.Chainable>
26 |
27 | /**
28 | * **Gets all user models for a specific userId via a GET request.**
29 | *
30 | * @param url string - The URL for getting all the models of a specific userId
31 | *
32 | * @example cy.getUserModelsViaApi('http://localhost:3000/models?userId=618f065ed18dc91b10651g98') // Gets all user models for userId=618f065ed18dc91b10651g98
33 | */
34 | getUserModelsViaApi(url: string): Cypress.Chainable>
35 |
36 | /**
37 | * **Deletes a model via a DELETE request.**
38 | *
39 | * @param modelId string - The id of a model (of the logged in user) that you want to delete
40 | *
41 | * @example cy.deleteModelViaApi('61a8db9bc67e9824b66fc8g2')
42 | */
43 | deleteModelViaApi(modelId: string): Cypress.Chainable>
44 |
45 | /**
46 | * **Creates a model via POST request.**
47 | *
48 | * @param type string - The type of the model you want to create. The allowed types are 'conceptual' and 'logical'
49 | * @param userId string - The id of the user for which you want to create the model
50 | * @param model object - An object with a cells property as an array, defining all "cells" of a model. Default is an object with an empty array as the value of the cells property
51 | *
52 | * @example cy.createModelViaApi('conceptual', '618f065ed18dc91b10650f99') // Creates a conceptual model for userId=618f065ed18dc91b10650f99
53 | * @example cy.createModelViaApi('logical', '618f065ed18dc91b10650f99') // Creates a logical model for userId=618f065ed18dc91b10650f99
54 | * @example cy.createModelViaApi('conceptual', '618f065ed18dc91b10650f99', conceptualModel) // Creates a conceptual model for userId=618f065ed18dc91b10650f99 passing a model object (`conceptualModel`), defined earlier, as a variable
55 | */
56 | createModelViaApi(type: string, userId: string, model?: object): Cypress.Chainable>
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/angular/logic/columnForm.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/angular/conceptual/conceptual.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
16 |
17 |
23 |
24 |
25 |
26 |
30 |
31 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/angular/logic/logic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
14 |
20 |
21 |
22 |
69 |
--------------------------------------------------------------------------------
/server_app/user/service.test.js:
--------------------------------------------------------------------------------
1 | const userService = require("./service");
2 |
3 | jest.mock("./model");
4 | jest.mock("../model/service");
5 | const UserRepositoryMock = require("./model");
6 | const ModelServiceMock = require("../model/service");
7 |
8 | afterEach(() => {
9 | jest.restoreAllMocks();
10 | });
11 |
12 | describe("login", () => {
13 |
14 | test('should return user when login params match', async () => {
15 | const userParam = {username: 'user@mail.com', password: "123456"};
16 |
17 | const docResponse = {
18 | "id": "507f191e810c19729de860ea",
19 | "name": "name"
20 | };
21 |
22 | UserRepositoryMock.findOne.mockResolvedValue(docResponse);
23 |
24 | const userSession = await userService.login(userParam);
25 |
26 | const expectedResponse = {
27 | "userId": "507f191e810c19729de860ea",
28 | "userName": "name"
29 | }
30 |
31 | expect(userSession).toMatchObject(expectedResponse);
32 | });
33 |
34 | test('should return null when login not found', async () => {
35 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"};
36 |
37 | UserRepositoryMock.findOne.mockResolvedValue(null);
38 |
39 | const userSession = await userService.login(userParam);
40 |
41 | expect(userSession).toBeNull();
42 | });
43 |
44 | test('should catch error when service fires it', async () => {
45 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"};
46 |
47 | UserRepositoryMock.findOne.mockResolvedValue(new Error("find error"));
48 |
49 | expect(() => {
50 | userService.login(userParam).toThrow();
51 | });
52 | });
53 |
54 | });
55 |
56 | describe("create", () => {
57 |
58 | test('should toThrow error when user already exists', async () => {
59 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"};
60 |
61 | const docResponse = {
62 | _id: '507f191e810c19729de860ea',
63 | name: 'name',
64 | login: 'user@mail.com',
65 | password: '123456'
66 | };
67 |
68 | UserRepositoryMock.findOne.mockResolvedValue(docResponse);
69 |
70 | expect(() => {
71 | userService.create(userParam).toThrow();
72 | });
73 |
74 | });
75 |
76 |
77 | test('should create user when it does not exists yet', async () => {
78 | const userParam = {username: 'name', password: "123456", mail:"user@mail.com"};
79 |
80 | const docResponse = {
81 | _id: '507f191e810c19729de860ea',
82 | name: 'name',
83 | login: 'user@mail.com',
84 | password: '123456'
85 | };
86 |
87 | UserRepositoryMock.findOne.mockResolvedValue(null);
88 |
89 | UserRepositoryMock.create.mockResolvedValue(docResponse);
90 |
91 | const createResponse = await userService.create(userParam);
92 |
93 | expect(createResponse.login).toBe(docResponse.login);
94 | });
95 |
96 | });
97 |
98 | describe("remove", () => {
99 |
100 | test('should toThrow error when user has models', async () => {
101 |
102 | ModelServiceMock.countAll.mockResolvedValue(1);
103 |
104 | expect(() => {
105 | userService.deleteAccount("123456").toThrow();
106 | });
107 |
108 | });
109 |
110 |
111 | test('should delete account', async () => {
112 | ModelServiceMock.countAll.mockResolvedValue(0);
113 |
114 | UserRepositoryMock.deleteOne.mockResolvedValue({"deletedCount": 1});
115 |
116 | const deleteResponse = await userService.deleteAccount("507f191e810c19729de860ea");
117 |
118 | expect(deleteResponse).toBe(true);
119 | });
120 |
121 | });
122 |
123 |
--------------------------------------------------------------------------------
/app/angular/workspace/workspace.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
42 |
43 |
44 | 
50 |
51 |
52 |
53 |
54 | | {{ 'Type' | translate }} |
55 | {{ 'Name' | translate }} |
56 | {{ 'Author' | translate }} |
57 | {{ 'Created' | translate }} |
58 | {{ 'Actions' | translate }} |
59 |
60 |
61 |
62 |
63 | |
64 |
65 |
66 | {{model.typeName}}
67 | |
68 |
69 |
70 | {{model.name}}
71 | |
72 | {{model.authorName}} |
73 | {{model.created | date:'dd/MM/yyyy'}} |
74 |
75 |
76 |
77 |
78 |
79 | |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/app/angular/publicview/publicview.js:
--------------------------------------------------------------------------------
1 | import "backbone";
2 | import $ from "jquery";
3 | import "./publicView.scss"
4 |
5 | import * as joint from "jointjs/dist/joint";
6 | import erd from "../../joint/shapes";
7 | import uml from "../../joint/table";
8 | joint.shapes.erd = erd;
9 | joint.shapes.uml = uml;
10 |
11 | import angular from "angular";
12 | import template from "./publicview.html";
13 |
14 | import "../editor/editorScroller";
15 |
16 | import statusBar from "../components/statusBar";
17 | import KeyboardController, { types } from "../components/keyboardController";
18 | import iconModelConceptual from "../components/icons/conceptual"
19 | import iconModelLogic from "../components/icons/logic"
20 |
21 | const controller = function (ModelAPI, $stateParams, $timeout, $state) {
22 | const ctrl = this;
23 | ctrl.loading = false;
24 | ctrl.model = {
25 | name: ""
26 | }
27 | const configs = {
28 | graph: {},
29 | paper: {}
30 | };
31 |
32 | ctrl.setLoading = (show) => {
33 | $timeout(() => {
34 | ctrl.loading = show;
35 | });
36 | }
37 |
38 | ctrl.print = () => {
39 | window.print();
40 | }
41 |
42 | ctrl.zoomIn = () => {
43 | configs.editorScroller.zoom(0.1, { max: 2 });
44 | }
45 |
46 | ctrl.zoomOut = () => {
47 | configs.editorScroller.zoom(-0.1, { min: 0.2 });
48 | }
49 |
50 | ctrl.zoomNone = () => {
51 | configs.editorScroller.zoom();
52 | }
53 |
54 | const registerShortcuts = () => {
55 | configs.keyboardController.registerHandler(types.ZOOM_IN, () => ctrl.zoomIn());
56 | configs.keyboardController.registerHandler(types.ZOOM_OUT, () => ctrl.zoomOut());
57 | configs.keyboardController.registerHandler(types.ZOOM_NONE, () => ctrl.zoomNone());
58 | }
59 |
60 | const buildWorkspace = () => {
61 | configs.graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes });
62 |
63 | const content = $("#content");
64 |
65 | configs.paper = new joint.dia.Paper({
66 | width: content.width(),
67 | height: content.height(),
68 | gridSize: 10,
69 | drawGrid: true,
70 | model: configs.graph,
71 | linkPinning: false,
72 | cellViewNamespace: joint.shapes
73 | });
74 |
75 | configs.keyboardController = new KeyboardController(configs.paper.$document);
76 |
77 | configs.editorScroller = new joint.ui.EditorScroller({
78 | paper: configs.paper,
79 | cursor: "grabbing",
80 | autoResizePaper: true,
81 | });
82 | content.append(configs.editorScroller.render().el);
83 |
84 | registerShortcuts();
85 | registerPaperEvents(configs.paper);
86 | };
87 |
88 | const registerPaperEvents = (paper) => {
89 | paper.on('blank:pointerdown', (evt) => {
90 | if(configs.keyboardController.spacePressed){
91 | configs.editorScroller.startPanning(evt);
92 | }
93 | });
94 | }
95 |
96 | ctrl.$postLink = () => {
97 | buildWorkspace();
98 | };
99 |
100 | ctrl.$onInit = () => {
101 | ctrl.setLoading(true);
102 |
103 | ModelAPI.getSharedModel($stateParams.modelshareid).then((resp) => {
104 | const jsonModel = (typeof resp.data.model == "string") ? JSON.parse(resp.data.model) : resp.data.model;
105 | if(resp.data.type === 'conceptual') {
106 | configs.paper.options.linkConnectionPoint = joint.util.shapePerimeterConnectionPoint;
107 | }
108 | configs.graph.fromJSON(jsonModel);
109 | ctrl.model.name = resp.data.name;
110 | ctrl.model.type = resp.data.type;
111 | ctrl.setLoading(false);
112 | configs.paper.freeze();
113 | }).catch((error) => {
114 | ctrl.setLoading(false);
115 | if(error.status == 404 || error.status == 401) {
116 | $state.go("noaccess");
117 | }
118 | });
119 | }
120 |
121 | ctrl.$onDestroy = () => {
122 | configs.graph = null;
123 | configs.paper = null;
124 | configs.keyboardController.unbindAll();
125 | configs.keyboardController = null;
126 | }
127 | };
128 |
129 | export default angular
130 | .module("app.publicview", [statusBar, iconModelConceptual, iconModelLogic])
131 | .component("publicview", {
132 | template,
133 | controller,
134 | }).name;
--------------------------------------------------------------------------------
/app/angular/components/queryExpressionModal/index.js:
--------------------------------------------------------------------------------
1 | import angular from "angular";
2 | import template from "./template.html";
3 | import "./styles.scss";
4 |
5 | const app = angular.module("app.queryExpressionModalController", []);
6 |
7 | const AND_OPERATOR = 'AND';
8 | const OR_OPERATOR = 'OR';
9 |
10 | const Controller = function ($filter) {
11 | const $ctrl = this;
12 | $ctrl.submitted = false;
13 | $ctrl.conditions = [];
14 | $ctrl.joins = [];
15 | $ctrl.tableColumns = [];
16 |
17 | const defaultJoin = {
18 | columnNameOrigin: null,
19 | columnNameTarget: null,
20 | }
21 |
22 | const defaultCondition = {
23 | columnName: null,
24 | columnType: null,
25 | type: null,
26 | comparativeValue: null,
27 | submitted: false,
28 | logicalOperator: AND_OPERATOR,
29 | };
30 |
31 | $ctrl.selectColumnJoin = (selected, attribute, index) => {
32 | const selectedJoin = $ctrl.joins[index];
33 | $ctrl.joins[index] = {
34 | ...selectedJoin,
35 | [attribute]: selected.name,
36 | };
37 | }
38 |
39 | $ctrl.saveJoin = (index) => {
40 | const selectedJoin = $ctrl.joins[index];
41 | $ctrl.joins[index] = {
42 | ...selectedJoin,
43 | submitted: true,
44 | };
45 | }
46 |
47 | $ctrl.removeJoin = (index) => {
48 | $ctrl.joins.splice(index, 1);
49 | }
50 |
51 | $ctrl.createLabel = condition => {
52 | return `${condition.columnName} ${$filter('translate')(condition.name).toLowerCase()} ${condition.comparativeValue} ${condition.comparativeValue2 ? `${$filter('translate')('and')} ${condition.comparativeValue2}` : '' }`;
53 | }
54 |
55 | $ctrl.changeOperator = (index) => {
56 | const selectedCondition = $ctrl.conditions[index];
57 | $ctrl.conditions[index] = {
58 | ...selectedCondition,
59 | logicalOperator: selectedCondition.logicalOperator === AND_OPERATOR ? OR_OPERATOR : AND_OPERATOR,
60 | };
61 | }
62 |
63 | $ctrl.selectComparasion = (selected, index) => {
64 | const selectedCondition = $ctrl.conditions[index];
65 | $ctrl.conditions[index] = {
66 | ...selectedCondition,
67 | name: selected.name,
68 | type: selected.type,
69 | };
70 | };
71 |
72 | $ctrl.removeCondition = (index) => {
73 | $ctrl.conditions.splice(index, 1);
74 | }
75 |
76 | $ctrl.saveCondition = (index) => {
77 | const selectedCondition = $ctrl.conditions[index];
78 | $ctrl.conditions[index] = {
79 | ...selectedCondition,
80 | submitted: true,
81 | };
82 | }
83 |
84 | $ctrl.selectColumn = (selected, index) => {
85 | const selectedCondition = $ctrl.conditions[index];
86 | $ctrl.conditions[index] = {
87 | ...selectedCondition,
88 | columnName: selected.name,
89 | columnType: selected.type
90 | };
91 | }
92 |
93 | $ctrl.addCondition = () => {
94 | $ctrl.conditions.push(defaultCondition);
95 | };
96 |
97 | $ctrl.addJoin = () => {
98 | $ctrl.joins.push(defaultJoin);
99 | }
100 |
101 | $ctrl.$onInit = () => {
102 | $ctrl.showJoins = $ctrl.tables.length > 1;
103 | $ctrl.tables.forEach(table => {
104 | table.columns.forEach(column => {
105 | $ctrl.tableColumns.push({
106 | name: `${table.name}.${column.name}`,
107 | type: column.type,
108 | });
109 | });
110 | });
111 | };
112 |
113 | $ctrl.$onChanges = (changes) => {
114 | if (changes.queryConditions != null && changes.queryConditions.currentValue != null) {
115 | $ctrl.conditions = changes.queryConditions.currentValue.values || [];
116 | $ctrl.joins = changes.queryConditions.currentValue.joins || [];
117 | }
118 | }
119 |
120 | $ctrl.save = function () {
121 | $ctrl.close({
122 | result: {
123 | conditions: $ctrl.conditions.filter(condition => condition.submitted),
124 | joins: $ctrl.joins.filter(join => join.submitted),
125 | }
126 | });
127 | };
128 |
129 | $ctrl.cancel = function () {
130 | $ctrl.dismiss({
131 | reason: "cancel",
132 | });
133 | };
134 | };
135 |
136 | export default app.component("queryExpressionModal", {
137 | template,
138 | bindings: {
139 | close: "&",
140 | dismiss: "&",
141 | tables: "<",
142 | queryConditions: '<'
143 | },
144 | controller: Controller,
145 | }).name;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brmodeloweb",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "node server.js",
7 | "start:dev": "export NODE_ENV=development && nodemon server.js",
8 | "start:frontend": "export NODE_ENV=development && webpack-dev-server --hot --progress --color",
9 | "build": "export NODE_ENV=production && rimraf ./app/dist && webpack --progress --color",
10 | "test": "jest --coverage",
11 | "test:cy": "yarn cypress run",
12 | "test:watch": "jest --watch",
13 | "cy:open": "yarn cypress open"
14 | },
15 | "engines": {
16 | "node": "20.x",
17 | "yarn": "1.x"
18 | },
19 | "dependencies": {
20 | "@babel/runtime": "^7.26.10",
21 | "@uirouter/angularjs": "^1.1.0",
22 | "angular": "^1.8.3",
23 | "angular-cookies": "^1.8.3",
24 | "angular-translate": "^2.18.3",
25 | "angular-ui-bootstrap": "^2.5.6",
26 | "angular-ui-router": "^1.0.30",
27 | "body-parser": "^1.20.2",
28 | "bootstrap": "^3.4.1",
29 | "cors": "^2.8.5",
30 | "dotenv": "^16.3.1",
31 | "errorhandler": "^1.5.1",
32 | "express": "^4.20.0",
33 | "express-session": "^1.17.3",
34 | "express-sslify": "^1.2.0",
35 | "font-awesome": "^4.7.0",
36 | "form-data": "^4.0.4",
37 | "hotkeys-js": "^3.12.0",
38 | "i18next": "^23.5.1",
39 | "i18next-browser-languagedetector": "^7.1.0",
40 | "jointjs": "3.7.5",
41 | "jsonwebtoken": "^9.0.2",
42 | "mailgun.js": "^12.0.1",
43 | "mongodb": "~4.17.0",
44 | "mongoose": "^6.13.6",
45 | "morgan": "^1.10.0",
46 | "oclazyload": "^1.1.0",
47 | "prop-types": "^15.8.1",
48 | "raw-body": "^2.5.2",
49 | "react": "^18.2.0",
50 | "react-dom": "^18.2.0",
51 | "react-i18next": "^13.2.2",
52 | "react2angular": "^4.0.6",
53 | "styled-components": "^6.0.8",
54 | "textangular": "^1.5.16",
55 | "typescript": "^5.2.2"
56 | },
57 | "devDependencies": {
58 | "@4tw/cypress-drag-drop": "^2.2.5",
59 | "@babel/core": "^7.23.0",
60 | "@babel/node": "^7.22.19",
61 | "@babel/plugin-proposal-class-properties": "^7.18.6",
62 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
63 | "@babel/plugin-transform-async-to-generator": "^7.22.5",
64 | "@babel/plugin-transform-runtime": "^7.22.15",
65 | "@babel/preset-env": "^7.22.20",
66 | "@babel/preset-react": "^7.22.15",
67 | "@faker-js/faker": "^8.2.0",
68 | "@types/react": "^18.2.24",
69 | "@types/styled-components": "^5.1.28",
70 | "@typescript-eslint/eslint-plugin": "^6.7.3",
71 | "@typescript-eslint/parser": "^6.7.3",
72 | "@webpack-cli/serve": "^2.0.5",
73 | "autoprefixer": "10.4.16",
74 | "babel-loader": "^9.1.3",
75 | "babel-plugin-angularjs-annotate": "^0.10.0",
76 | "css-loader": "^6.8.1",
77 | "cssnano": "6.0.1",
78 | "cypress": "^13.3.0",
79 | "dotenv-webpack": "^8.0.1",
80 | "eslint": "^8.50.0",
81 | "eslint-config-airbnb": "^19.0.4",
82 | "eslint-config-airbnb-base": "^15.0.0",
83 | "eslint-config-prettier": "^9.0.0",
84 | "eslint-import-resolver-typescript": "^3.6.1",
85 | "eslint-plugin-cypress": "^2.15.1",
86 | "eslint-plugin-import": "^2.28.1",
87 | "eslint-plugin-prettier": "^5.0.0",
88 | "eslint-plugin-react": "^7.33.2",
89 | "file-loader": "^6.2.0",
90 | "html-loader": "^4.2.0",
91 | "html-webpack-plugin": "5.5.3",
92 | "jest": "^29.7.0",
93 | "mini-css-extract-plugin": "^2.7.6",
94 | "nodemon": "^3.0.1",
95 | "postcss-loader": "^7.3.3",
96 | "prettier": "^3.0.3",
97 | "raw-loader": "^4.0.2",
98 | "rimraf": "^5.0.5",
99 | "sass": "^1.68.0",
100 | "sass-loader": "^13.3.2",
101 | "style-loader": "^3.3.3",
102 | "supertest": "^6.3.3",
103 | "terser-webpack-plugin": "^5.3.9",
104 | "ts-loader": "^9.4.4",
105 | "tsconfig-paths-webpack-plugin": "^4.1.0",
106 | "url-loader": "^4.1.1",
107 | "webpack": "5.94.0",
108 | "webpack-cli": "^5.1.4",
109 | "webpack-dev-server": "^5.2.1"
110 | },
111 | "resolutions": {
112 | "styled-components": "^5"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/sass/mainHeader.scss:
--------------------------------------------------------------------------------
1 | ///////////////////////////////////////////////////////////////////////////////
2 | // .navbar-brand
3 | ///////////////////////////////////////////////////////////////////////////////
4 | .navbar-default .navbar-brand,
5 | .navbar-default .navbar-brand:hover {
6 | color: var(--brand-default);
7 | cursor: default;
8 | padding-right: 0;
9 | }
10 |
11 | ///////////////////////////////////////////////////////////////////////////////
12 | // .nav-bar .back-button
13 | ///////////////////////////////////////////////////////////////////////////////
14 | .navbar-default .back-button {
15 | display: block;
16 | float: left;
17 | color: var(--brand-default);
18 | font-size: 1.2em;
19 | padding: 13px 16px;
20 | }
21 |
22 | .navbar-default .back-button:hover {
23 | color: var(--brand-default);
24 | background-color: #e1e1e1;
25 | }
26 |
27 | ///////////////////////////////////////////////////////////////////////////////
28 | // purelly icon nav
29 | ///////////////////////////////////////////////////////////////////////////////
30 | .nav-icons .fa {
31 | font-size: 1.5em;
32 | }
33 |
34 | ///////////////////////////////////////////////////////////////////////////////
35 | // toolbar with icons
36 | ///////////////////////////////////////////////////////////////////////////////
37 | .tool-icons {
38 | float: none;
39 | width: 100%;
40 | border-bottom: 1px solid var(--border-default);
41 | }
42 |
43 | .tool-icons .fa {
44 | font-size: 1.2em;
45 | }
46 |
47 | .navbar-nav.tool-icons > li > a {
48 | padding: 8px 12px;
49 | }
50 |
51 | .tool-icons .divider {
52 | border-right: 1px solid var(--border-default);
53 | }
54 |
55 | ////////////////////////////////////////////////////////////////////////////////
56 | // .modelOptions
57 | ////////////////////////////////////////////////////////////////////////////////
58 | .modelOptions {float: left;}
59 |
60 | .modelName,
61 | .modelViewiewOptions {
62 | float: left;
63 | padding: 8px 0;
64 | margin: 0 10px;
65 | }
66 |
67 | .modelName input {
68 | min-width: 250px;
69 | padding: 3px 25px 3px 5px;
70 | border-color: 1px solid var(--border-default);
71 | background-color: rgba(255, 255, 255, 0.1);
72 | color: grey;
73 | }
74 |
75 | .inputPencil {
76 | float: right;
77 | margin-right: 6px;
78 | margin-top: -20px;
79 | position: relative;
80 | z-index: 2;
81 | color: grey;
82 | }
83 |
84 | ////////////////////////////////////////////////////////////////////////////////
85 | // .modelViewiewOptions
86 | ////////////////////////////////////////////////////////////////////////////////
87 | .modelViewiewOptions {
88 | position: relative;
89 | top: 8px;
90 | }
91 |
92 | .modelViewiewOptions a {
93 | padding: 5px 10px 5px 10px;
94 | color: black;
95 | position: relative;
96 | }
97 |
98 | .modelViewiewOptions a:hover {opacity: 0.6;}
99 | .modelViewiewOptions a:active {opacity: 0.2;}
100 |
101 | ////////////////////////////////////////////////////////////////////////////////
102 | // Loading
103 | ////////////////////////////////////////////////////////////////////////////////
104 | .br-loader {
105 | display: none;
106 | position: fixed;
107 | top: 70px;
108 | left: 50%;
109 | margin-left: -20px;
110 | padding: 3px;;
111 | border: 1px solid var(--border-light);
112 | border-radius: 4px;
113 | background-color: var(--white);
114 | }
115 |
116 | .br-loader.loading {display: block;}
117 |
118 | ///////////////////////////////////////////////////////////////////////////////
119 | // .nav-icons dropdown-icon
120 | ///////////////////////////////////////////////////////////////////////////////
121 | .nav-icons > dropdown-icon > .dropdown > .dropdown-menu {
122 | margin-top: 0;
123 | border-top-left-radius: 0;
124 | border-top-right-radius: 0;
125 | }
126 |
127 | .nav-icons > dropdown-icon > .dropdown > button {
128 | border: 0;
129 | padding: 15px;
130 | color: var(--gray-20);
131 | background-color: transparent;
132 | }
133 |
134 | .nav-icons > dropdown-icon > .dropdown > button:focus {
135 | text-decoration: none;
136 | box-shadow: none;
137 | background-color: var(--gray-90);
138 | }
139 |
140 | .nav-icons .dropdown-menu > li > a {
141 | color: var(--gray-20);
142 | }
--------------------------------------------------------------------------------
|