├── .github └── workflows │ ├── build-pr.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── sample ├── exportPlantUML.ts ├── index.ts ├── package-lock.json ├── package.json ├── pushWorkspace.ts ├── tsconfig.json └── workspace.ts ├── src ├── client │ ├── api │ │ ├── hmac.ts │ │ ├── httpClient.ts │ │ └── structurizrClient.ts │ ├── index.ts │ └── plantUML │ │ └── plantUMLWriter.ts ├── core │ ├── documentation │ │ ├── decision.ts │ │ ├── decisionStatus.ts │ │ ├── documentation.ts │ │ ├── format.ts │ │ └── section.ts │ ├── index.ts │ ├── model │ │ ├── abstractImpliedRelationshipsStrategy.ts │ │ ├── codeElement.ts │ │ ├── component.ts │ │ ├── container.ts │ │ ├── containerInstance.ts │ │ ├── createImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.ts │ │ ├── createImpliedRelationshipsUnlessSameRelationshipExistsStrategy.ts │ │ ├── defaultImpliedRelationshipsStrategy.ts │ │ ├── deploymentElement.ts │ │ ├── deploymentNode.ts │ │ ├── element.ts │ │ ├── httpHealthCheck.ts │ │ ├── iequatable.ts │ │ ├── impliedRelationshipsStrategy.ts │ │ ├── interactionStyle.ts │ │ ├── isChildOf.ts │ │ ├── location.ts │ │ ├── model.ts │ │ ├── modelItem.ts │ │ ├── person.ts │ │ ├── relationship.ts │ │ ├── relationships.ts │ │ ├── role.ts │ │ ├── sequentialIntegerIdGeneratorStrategy.ts │ │ ├── softwareSystem.ts │ │ ├── staticStructureElement.ts │ │ ├── tags.ts │ │ ├── user.ts │ │ ├── workspace.ts │ │ └── workspaceConfiguration.ts │ └── view │ │ ├── automaticLayout.ts │ │ ├── branding.ts │ │ ├── componentView.ts │ │ ├── containerView.ts │ │ ├── deploymentView.ts │ │ ├── elementView.ts │ │ ├── filteredView.ts │ │ ├── paperSize.ts │ │ ├── rankDirection.ts │ │ ├── relationshipView.ts │ │ ├── staticView.ts │ │ ├── styles.ts │ │ ├── systemContextView.ts │ │ ├── terminology.ts │ │ ├── view.ts │ │ ├── viewConfiguration.ts │ │ └── viewSet.ts └── index.ts ├── test ├── api-compatibility.ts ├── delivers.ts ├── implicitRelationships.ts ├── index.ts ├── plantUMLWriter.ts ├── regression.ts ├── themeExport.ts └── workspace.ts └── tsconfig.json /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | name: build PRs 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build-and-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Use Node.js 14.4.0 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 14.4.0 14 | registry-url: https://registry.npmjs.org/ 15 | - name: npm install 16 | run: npm i 17 | - name: npm test 18 | run: npm t 19 | env: 20 | STRUCTURIZR_WORKSPACE_ID: ${{secrets.STRUCTURIZR_WORKSPACE_ID}} 21 | STRUCTURIZR_API_KEY: ${{secrets.STRUCTURIZR_API_KEY}} 22 | STRUCTURIZR_API_SECRET: ${{secrets.STRUCTURIZR_API_SECRET}} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js 14.4.0 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 14.4.0 17 | registry-url: https://registry.npmjs.org/ 18 | - name: npm install 19 | run: npm i 20 | - name: npm test 21 | run: npm t 22 | env: 23 | STRUCTURIZR_WORKSPACE_ID: ${{secrets.STRUCTURIZR_WORKSPACE_ID}} 24 | STRUCTURIZR_API_KEY: ${{secrets.STRUCTURIZR_API_KEY}} 25 | STRUCTURIZR_API_SECRET: ${{secrets.STRUCTURIZR_API_SECRET}} 26 | - name: npm build 27 | if: github.ref == 'refs/heads/master' 28 | run: npm run build 29 | - name: publish to npmjs 30 | if: github.ref == 'refs/heads/master' 31 | run: npm publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | sample/node_modules/ 4 | sample/dist/ 5 | .env -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\dist\\dummy.js", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "outFiles": [ 14 | "${workspaceFolder}/dist/**/*.js" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Structurizr for TypeScript 2 | 3 | This GitHub repository is a port of [Structurizr for .NET](https://github.com/structurizr/dotnet) to TypeScript, in order to help you visualise, document and explore the software architecture of a software system. In summary, it allows you to create a software architecture model based upon Simon Brown's [C4 model](https://structurizr.com/help/c4). 4 | 5 | [![npm version](https://badge.fury.io/js/structurizr-typescript.svg)](https://www.npmjs.com/package/structurizr-typescript) 6 | 7 | [![Actions Status](https://github.com/ChristianEder/structurizr-typescript/workflows/npm%20publish/badge.svg)](https://github.com/ChristianEder/structurizr-typescript/actions) 8 | 9 | ## How to use 10 | 11 | - Set up a new project similar to this [sample](https://github.com/ChristianEder/structurizr-typescript/tree/master/sample) 12 | > npm init\ 13 | > npm install -D @types/node\ 14 | > npm install -D typescript\ 15 | > npm install -S structurizr-typescript 16 | - Start coding your architecture model similar to the [sample index.ts](https://github.com/ChristianEder/structurizr-typescript/blob/master/sample/index.ts) 17 | - For more detailed documentation on how to use Structurizr, please refer to [Structurizr for .NET](https://github.com/structurizr/dotnet) - the usage is pretty much the same 18 | - In the current version of this package, not all of Structurizrs features are already implemented. See [Limitations](#Limitations) section below 19 | 20 | ## Limitations 21 | 22 | The current version of this package supports: 23 | - Person, SoftwareSystem, Container, Component & CodeElement entities 24 | - Relationships between those entities 25 | - System Context, Container & Component diagrams 26 | - Deployment diagrams with DeploymentNode, ContainerInstance and HttpHealthCheck entities 27 | - Filtered Views 28 | - Custom Element & Relationship styles, usage & export of themes 29 | - Documentation Sections & Decisions (kudos go to [Joe Ruello](https://github.com/joeruello)) 30 | - Diagram Autolayouting (kudos go to [Joe Ruello](https://github.com/joeruello)) 31 | - Diagram paper size settings 32 | - Custom Branding & Terminology 33 | 34 | This specifically excludes: 35 | - Encrypted workspaces 36 | - Dynamic diagrams 37 | - Enterprise context diagrams 38 | 39 | Also, as of now the package has just a few automated tests - use at own risk :-). Let me know if you encounter any issues, I am happy to provide a fix. 40 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structurizr-typescript", 3 | "version": "1.0.13", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "structurizr-typescript", 9 | "version": "1.0.13", 10 | "dependencies": { 11 | "crypto-js": "4.2.0" 12 | }, 13 | "devDependencies": { 14 | "@types/chai": "^4.2.0", 15 | "@types/crypto-js": "4.2.2", 16 | "@types/deep-equal-in-any-order": "1.0.3", 17 | "@types/mocha": "^9.0.0", 18 | "@types/node": "20.11.24", 19 | "chai": "^4.3.0", 20 | "deep-equal-in-any-order": "2.0.6", 21 | "mocha": "^9.1.0", 22 | "ts-node": "^10.0.0", 23 | "typescript": "^4.0.0" 24 | } 25 | }, 26 | "node_modules/@cspotcode/source-map-support": { 27 | "version": "0.8.1", 28 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 29 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 30 | "dev": true, 31 | "dependencies": { 32 | "@jridgewell/trace-mapping": "0.3.9" 33 | }, 34 | "engines": { 35 | "node": ">=12" 36 | } 37 | }, 38 | "node_modules/@jridgewell/resolve-uri": { 39 | "version": "3.1.2", 40 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 41 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 42 | "dev": true, 43 | "engines": { 44 | "node": ">=6.0.0" 45 | } 46 | }, 47 | "node_modules/@jridgewell/sourcemap-codec": { 48 | "version": "1.4.15", 49 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 50 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 51 | "dev": true 52 | }, 53 | "node_modules/@jridgewell/trace-mapping": { 54 | "version": "0.3.9", 55 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 56 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 57 | "dev": true, 58 | "dependencies": { 59 | "@jridgewell/resolve-uri": "^3.0.3", 60 | "@jridgewell/sourcemap-codec": "^1.4.10" 61 | } 62 | }, 63 | "node_modules/@tsconfig/node10": { 64 | "version": "1.0.9", 65 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 66 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 67 | "dev": true 68 | }, 69 | "node_modules/@tsconfig/node12": { 70 | "version": "1.0.11", 71 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 72 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 73 | "dev": true 74 | }, 75 | "node_modules/@tsconfig/node14": { 76 | "version": "1.0.3", 77 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 78 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 79 | "dev": true 80 | }, 81 | "node_modules/@tsconfig/node16": { 82 | "version": "1.0.4", 83 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 84 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 85 | "dev": true 86 | }, 87 | "node_modules/@types/chai": { 88 | "version": "4.3.12", 89 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.12.tgz", 90 | "integrity": "sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==", 91 | "dev": true 92 | }, 93 | "node_modules/@types/crypto-js": { 94 | "version": "4.2.2", 95 | "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", 96 | "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", 97 | "dev": true 98 | }, 99 | "node_modules/@types/deep-equal-in-any-order": { 100 | "version": "1.0.3", 101 | "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.3.tgz", 102 | "integrity": "sha512-jT0O3hAILDKeKbdWJ9FZLD0Xdfhz7hMvfyFlRWpirjiEVr8G+GZ4kVIzPIqM6x6Rpp93TNPgOAed4XmvcuV6Qg==", 103 | "dev": true 104 | }, 105 | "node_modules/@types/mocha": { 106 | "version": "9.1.1", 107 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", 108 | "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", 109 | "dev": true 110 | }, 111 | "node_modules/@types/node": { 112 | "version": "20.11.24", 113 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", 114 | "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", 115 | "dev": true, 116 | "dependencies": { 117 | "undici-types": "~5.26.4" 118 | } 119 | }, 120 | "node_modules/@ungap/promise-all-settled": { 121 | "version": "1.1.2", 122 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 123 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 124 | "dev": true 125 | }, 126 | "node_modules/acorn": { 127 | "version": "8.11.3", 128 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", 129 | "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", 130 | "dev": true, 131 | "bin": { 132 | "acorn": "bin/acorn" 133 | }, 134 | "engines": { 135 | "node": ">=0.4.0" 136 | } 137 | }, 138 | "node_modules/acorn-walk": { 139 | "version": "8.3.2", 140 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 141 | "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 142 | "dev": true, 143 | "engines": { 144 | "node": ">=0.4.0" 145 | } 146 | }, 147 | "node_modules/ansi-colors": { 148 | "version": "4.1.1", 149 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 150 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 151 | "dev": true, 152 | "engines": { 153 | "node": ">=6" 154 | } 155 | }, 156 | "node_modules/ansi-regex": { 157 | "version": "5.0.1", 158 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 159 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 160 | "dev": true, 161 | "engines": { 162 | "node": ">=8" 163 | } 164 | }, 165 | "node_modules/ansi-styles": { 166 | "version": "4.3.0", 167 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 168 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 169 | "dev": true, 170 | "dependencies": { 171 | "color-convert": "^2.0.1" 172 | }, 173 | "engines": { 174 | "node": ">=8" 175 | }, 176 | "funding": { 177 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 178 | } 179 | }, 180 | "node_modules/anymatch": { 181 | "version": "3.1.3", 182 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 183 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 184 | "dev": true, 185 | "dependencies": { 186 | "normalize-path": "^3.0.0", 187 | "picomatch": "^2.0.4" 188 | }, 189 | "engines": { 190 | "node": ">= 8" 191 | } 192 | }, 193 | "node_modules/arg": { 194 | "version": "4.1.3", 195 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 196 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 197 | "dev": true 198 | }, 199 | "node_modules/argparse": { 200 | "version": "2.0.1", 201 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 202 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 203 | "dev": true 204 | }, 205 | "node_modules/assertion-error": { 206 | "version": "1.1.0", 207 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 208 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 209 | "dev": true, 210 | "engines": { 211 | "node": "*" 212 | } 213 | }, 214 | "node_modules/balanced-match": { 215 | "version": "1.0.2", 216 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 217 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 218 | "dev": true 219 | }, 220 | "node_modules/binary-extensions": { 221 | "version": "2.2.0", 222 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 223 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 224 | "dev": true, 225 | "engines": { 226 | "node": ">=8" 227 | } 228 | }, 229 | "node_modules/brace-expansion": { 230 | "version": "1.1.11", 231 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 232 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 233 | "dev": true, 234 | "dependencies": { 235 | "balanced-match": "^1.0.0", 236 | "concat-map": "0.0.1" 237 | } 238 | }, 239 | "node_modules/braces": { 240 | "version": "3.0.2", 241 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 242 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 243 | "dev": true, 244 | "dependencies": { 245 | "fill-range": "^7.0.1" 246 | }, 247 | "engines": { 248 | "node": ">=8" 249 | } 250 | }, 251 | "node_modules/browser-stdout": { 252 | "version": "1.3.1", 253 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 254 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 255 | "dev": true 256 | }, 257 | "node_modules/camelcase": { 258 | "version": "6.3.0", 259 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 260 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 261 | "dev": true, 262 | "engines": { 263 | "node": ">=10" 264 | }, 265 | "funding": { 266 | "url": "https://github.com/sponsors/sindresorhus" 267 | } 268 | }, 269 | "node_modules/chai": { 270 | "version": "4.4.1", 271 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", 272 | "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", 273 | "dev": true, 274 | "dependencies": { 275 | "assertion-error": "^1.1.0", 276 | "check-error": "^1.0.3", 277 | "deep-eql": "^4.1.3", 278 | "get-func-name": "^2.0.2", 279 | "loupe": "^2.3.6", 280 | "pathval": "^1.1.1", 281 | "type-detect": "^4.0.8" 282 | }, 283 | "engines": { 284 | "node": ">=4" 285 | } 286 | }, 287 | "node_modules/chalk": { 288 | "version": "4.1.2", 289 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 290 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 291 | "dev": true, 292 | "dependencies": { 293 | "ansi-styles": "^4.1.0", 294 | "supports-color": "^7.1.0" 295 | }, 296 | "engines": { 297 | "node": ">=10" 298 | }, 299 | "funding": { 300 | "url": "https://github.com/chalk/chalk?sponsor=1" 301 | } 302 | }, 303 | "node_modules/chalk/node_modules/supports-color": { 304 | "version": "7.2.0", 305 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 306 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 307 | "dev": true, 308 | "dependencies": { 309 | "has-flag": "^4.0.0" 310 | }, 311 | "engines": { 312 | "node": ">=8" 313 | } 314 | }, 315 | "node_modules/check-error": { 316 | "version": "1.0.3", 317 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", 318 | "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", 319 | "dev": true, 320 | "dependencies": { 321 | "get-func-name": "^2.0.2" 322 | }, 323 | "engines": { 324 | "node": "*" 325 | } 326 | }, 327 | "node_modules/chokidar": { 328 | "version": "3.5.3", 329 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 330 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 331 | "dev": true, 332 | "funding": [ 333 | { 334 | "type": "individual", 335 | "url": "https://paulmillr.com/funding/" 336 | } 337 | ], 338 | "dependencies": { 339 | "anymatch": "~3.1.2", 340 | "braces": "~3.0.2", 341 | "glob-parent": "~5.1.2", 342 | "is-binary-path": "~2.1.0", 343 | "is-glob": "~4.0.1", 344 | "normalize-path": "~3.0.0", 345 | "readdirp": "~3.6.0" 346 | }, 347 | "engines": { 348 | "node": ">= 8.10.0" 349 | }, 350 | "optionalDependencies": { 351 | "fsevents": "~2.3.2" 352 | } 353 | }, 354 | "node_modules/cliui": { 355 | "version": "7.0.4", 356 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 357 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 358 | "dev": true, 359 | "dependencies": { 360 | "string-width": "^4.2.0", 361 | "strip-ansi": "^6.0.0", 362 | "wrap-ansi": "^7.0.0" 363 | } 364 | }, 365 | "node_modules/color-convert": { 366 | "version": "2.0.1", 367 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 368 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 369 | "dev": true, 370 | "dependencies": { 371 | "color-name": "~1.1.4" 372 | }, 373 | "engines": { 374 | "node": ">=7.0.0" 375 | } 376 | }, 377 | "node_modules/color-name": { 378 | "version": "1.1.4", 379 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 380 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 381 | "dev": true 382 | }, 383 | "node_modules/concat-map": { 384 | "version": "0.0.1", 385 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 386 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 387 | "dev": true 388 | }, 389 | "node_modules/create-require": { 390 | "version": "1.1.1", 391 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 392 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 393 | "dev": true 394 | }, 395 | "node_modules/crypto-js": { 396 | "version": "4.2.0", 397 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", 398 | "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" 399 | }, 400 | "node_modules/debug": { 401 | "version": "4.3.3", 402 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 403 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 404 | "dev": true, 405 | "dependencies": { 406 | "ms": "2.1.2" 407 | }, 408 | "engines": { 409 | "node": ">=6.0" 410 | }, 411 | "peerDependenciesMeta": { 412 | "supports-color": { 413 | "optional": true 414 | } 415 | } 416 | }, 417 | "node_modules/debug/node_modules/ms": { 418 | "version": "2.1.2", 419 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 420 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 421 | "dev": true 422 | }, 423 | "node_modules/decamelize": { 424 | "version": "4.0.0", 425 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 426 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 427 | "dev": true, 428 | "engines": { 429 | "node": ">=10" 430 | }, 431 | "funding": { 432 | "url": "https://github.com/sponsors/sindresorhus" 433 | } 434 | }, 435 | "node_modules/deep-eql": { 436 | "version": "4.1.3", 437 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", 438 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", 439 | "dev": true, 440 | "dependencies": { 441 | "type-detect": "^4.0.0" 442 | }, 443 | "engines": { 444 | "node": ">=6" 445 | } 446 | }, 447 | "node_modules/deep-equal-in-any-order": { 448 | "version": "2.0.6", 449 | "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", 450 | "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", 451 | "dev": true, 452 | "dependencies": { 453 | "lodash.mapvalues": "^4.6.0", 454 | "sort-any": "^2.0.0" 455 | } 456 | }, 457 | "node_modules/diff": { 458 | "version": "5.0.0", 459 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 460 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 461 | "dev": true, 462 | "engines": { 463 | "node": ">=0.3.1" 464 | } 465 | }, 466 | "node_modules/emoji-regex": { 467 | "version": "8.0.0", 468 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 469 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 470 | "dev": true 471 | }, 472 | "node_modules/escalade": { 473 | "version": "3.1.2", 474 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 475 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 476 | "dev": true, 477 | "engines": { 478 | "node": ">=6" 479 | } 480 | }, 481 | "node_modules/escape-string-regexp": { 482 | "version": "4.0.0", 483 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 484 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 485 | "dev": true, 486 | "engines": { 487 | "node": ">=10" 488 | }, 489 | "funding": { 490 | "url": "https://github.com/sponsors/sindresorhus" 491 | } 492 | }, 493 | "node_modules/fill-range": { 494 | "version": "7.0.1", 495 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 496 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 497 | "dev": true, 498 | "dependencies": { 499 | "to-regex-range": "^5.0.1" 500 | }, 501 | "engines": { 502 | "node": ">=8" 503 | } 504 | }, 505 | "node_modules/find-up": { 506 | "version": "5.0.0", 507 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 508 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 509 | "dev": true, 510 | "dependencies": { 511 | "locate-path": "^6.0.0", 512 | "path-exists": "^4.0.0" 513 | }, 514 | "engines": { 515 | "node": ">=10" 516 | }, 517 | "funding": { 518 | "url": "https://github.com/sponsors/sindresorhus" 519 | } 520 | }, 521 | "node_modules/flat": { 522 | "version": "5.0.2", 523 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 524 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 525 | "dev": true, 526 | "bin": { 527 | "flat": "cli.js" 528 | } 529 | }, 530 | "node_modules/fs.realpath": { 531 | "version": "1.0.0", 532 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 533 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 534 | "dev": true 535 | }, 536 | "node_modules/fsevents": { 537 | "version": "2.3.3", 538 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 539 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 540 | "dev": true, 541 | "hasInstallScript": true, 542 | "optional": true, 543 | "os": [ 544 | "darwin" 545 | ], 546 | "engines": { 547 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 548 | } 549 | }, 550 | "node_modules/get-caller-file": { 551 | "version": "2.0.5", 552 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 553 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 554 | "dev": true, 555 | "engines": { 556 | "node": "6.* || 8.* || >= 10.*" 557 | } 558 | }, 559 | "node_modules/get-func-name": { 560 | "version": "2.0.2", 561 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", 562 | "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", 563 | "dev": true, 564 | "engines": { 565 | "node": "*" 566 | } 567 | }, 568 | "node_modules/glob": { 569 | "version": "7.2.0", 570 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 571 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 572 | "dev": true, 573 | "dependencies": { 574 | "fs.realpath": "^1.0.0", 575 | "inflight": "^1.0.4", 576 | "inherits": "2", 577 | "minimatch": "^3.0.4", 578 | "once": "^1.3.0", 579 | "path-is-absolute": "^1.0.0" 580 | }, 581 | "engines": { 582 | "node": "*" 583 | }, 584 | "funding": { 585 | "url": "https://github.com/sponsors/isaacs" 586 | } 587 | }, 588 | "node_modules/glob-parent": { 589 | "version": "5.1.2", 590 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 591 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 592 | "dev": true, 593 | "dependencies": { 594 | "is-glob": "^4.0.1" 595 | }, 596 | "engines": { 597 | "node": ">= 6" 598 | } 599 | }, 600 | "node_modules/glob/node_modules/minimatch": { 601 | "version": "3.1.2", 602 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 603 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 604 | "dev": true, 605 | "dependencies": { 606 | "brace-expansion": "^1.1.7" 607 | }, 608 | "engines": { 609 | "node": "*" 610 | } 611 | }, 612 | "node_modules/growl": { 613 | "version": "1.10.5", 614 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 615 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 616 | "dev": true, 617 | "engines": { 618 | "node": ">=4.x" 619 | } 620 | }, 621 | "node_modules/has-flag": { 622 | "version": "4.0.0", 623 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 624 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 625 | "dev": true, 626 | "engines": { 627 | "node": ">=8" 628 | } 629 | }, 630 | "node_modules/he": { 631 | "version": "1.2.0", 632 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 633 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 634 | "dev": true, 635 | "bin": { 636 | "he": "bin/he" 637 | } 638 | }, 639 | "node_modules/inflight": { 640 | "version": "1.0.6", 641 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 642 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 643 | "dev": true, 644 | "dependencies": { 645 | "once": "^1.3.0", 646 | "wrappy": "1" 647 | } 648 | }, 649 | "node_modules/inherits": { 650 | "version": "2.0.4", 651 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 652 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 653 | "dev": true 654 | }, 655 | "node_modules/is-binary-path": { 656 | "version": "2.1.0", 657 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 658 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 659 | "dev": true, 660 | "dependencies": { 661 | "binary-extensions": "^2.0.0" 662 | }, 663 | "engines": { 664 | "node": ">=8" 665 | } 666 | }, 667 | "node_modules/is-extglob": { 668 | "version": "2.1.1", 669 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 670 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 671 | "dev": true, 672 | "engines": { 673 | "node": ">=0.10.0" 674 | } 675 | }, 676 | "node_modules/is-fullwidth-code-point": { 677 | "version": "3.0.0", 678 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 679 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 680 | "dev": true, 681 | "engines": { 682 | "node": ">=8" 683 | } 684 | }, 685 | "node_modules/is-glob": { 686 | "version": "4.0.3", 687 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 688 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 689 | "dev": true, 690 | "dependencies": { 691 | "is-extglob": "^2.1.1" 692 | }, 693 | "engines": { 694 | "node": ">=0.10.0" 695 | } 696 | }, 697 | "node_modules/is-number": { 698 | "version": "7.0.0", 699 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 700 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 701 | "dev": true, 702 | "engines": { 703 | "node": ">=0.12.0" 704 | } 705 | }, 706 | "node_modules/is-plain-obj": { 707 | "version": "2.1.0", 708 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 709 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 710 | "dev": true, 711 | "engines": { 712 | "node": ">=8" 713 | } 714 | }, 715 | "node_modules/is-unicode-supported": { 716 | "version": "0.1.0", 717 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 718 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 719 | "dev": true, 720 | "engines": { 721 | "node": ">=10" 722 | }, 723 | "funding": { 724 | "url": "https://github.com/sponsors/sindresorhus" 725 | } 726 | }, 727 | "node_modules/isexe": { 728 | "version": "2.0.0", 729 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 730 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 731 | "dev": true 732 | }, 733 | "node_modules/js-yaml": { 734 | "version": "4.1.0", 735 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 736 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 737 | "dev": true, 738 | "dependencies": { 739 | "argparse": "^2.0.1" 740 | }, 741 | "bin": { 742 | "js-yaml": "bin/js-yaml.js" 743 | } 744 | }, 745 | "node_modules/locate-path": { 746 | "version": "6.0.0", 747 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 748 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 749 | "dev": true, 750 | "dependencies": { 751 | "p-locate": "^5.0.0" 752 | }, 753 | "engines": { 754 | "node": ">=10" 755 | }, 756 | "funding": { 757 | "url": "https://github.com/sponsors/sindresorhus" 758 | } 759 | }, 760 | "node_modules/lodash": { 761 | "version": "4.17.21", 762 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 763 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 764 | "dev": true 765 | }, 766 | "node_modules/lodash.mapvalues": { 767 | "version": "4.6.0", 768 | "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", 769 | "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", 770 | "dev": true 771 | }, 772 | "node_modules/log-symbols": { 773 | "version": "4.1.0", 774 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 775 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 776 | "dev": true, 777 | "dependencies": { 778 | "chalk": "^4.1.0", 779 | "is-unicode-supported": "^0.1.0" 780 | }, 781 | "engines": { 782 | "node": ">=10" 783 | }, 784 | "funding": { 785 | "url": "https://github.com/sponsors/sindresorhus" 786 | } 787 | }, 788 | "node_modules/loupe": { 789 | "version": "2.3.7", 790 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", 791 | "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", 792 | "dev": true, 793 | "dependencies": { 794 | "get-func-name": "^2.0.1" 795 | } 796 | }, 797 | "node_modules/make-error": { 798 | "version": "1.3.5", 799 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 800 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 801 | "dev": true 802 | }, 803 | "node_modules/minimatch": { 804 | "version": "4.2.1", 805 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", 806 | "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", 807 | "dev": true, 808 | "dependencies": { 809 | "brace-expansion": "^1.1.7" 810 | }, 811 | "engines": { 812 | "node": ">=10" 813 | } 814 | }, 815 | "node_modules/mocha": { 816 | "version": "9.2.2", 817 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", 818 | "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", 819 | "dev": true, 820 | "dependencies": { 821 | "@ungap/promise-all-settled": "1.1.2", 822 | "ansi-colors": "4.1.1", 823 | "browser-stdout": "1.3.1", 824 | "chokidar": "3.5.3", 825 | "debug": "4.3.3", 826 | "diff": "5.0.0", 827 | "escape-string-regexp": "4.0.0", 828 | "find-up": "5.0.0", 829 | "glob": "7.2.0", 830 | "growl": "1.10.5", 831 | "he": "1.2.0", 832 | "js-yaml": "4.1.0", 833 | "log-symbols": "4.1.0", 834 | "minimatch": "4.2.1", 835 | "ms": "2.1.3", 836 | "nanoid": "3.3.1", 837 | "serialize-javascript": "6.0.0", 838 | "strip-json-comments": "3.1.1", 839 | "supports-color": "8.1.1", 840 | "which": "2.0.2", 841 | "workerpool": "6.2.0", 842 | "yargs": "16.2.0", 843 | "yargs-parser": "20.2.4", 844 | "yargs-unparser": "2.0.0" 845 | }, 846 | "bin": { 847 | "_mocha": "bin/_mocha", 848 | "mocha": "bin/mocha" 849 | }, 850 | "engines": { 851 | "node": ">= 12.0.0" 852 | }, 853 | "funding": { 854 | "type": "opencollective", 855 | "url": "https://opencollective.com/mochajs" 856 | } 857 | }, 858 | "node_modules/ms": { 859 | "version": "2.1.3", 860 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 861 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 862 | "dev": true 863 | }, 864 | "node_modules/nanoid": { 865 | "version": "3.3.1", 866 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", 867 | "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", 868 | "dev": true, 869 | "bin": { 870 | "nanoid": "bin/nanoid.cjs" 871 | }, 872 | "engines": { 873 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 874 | } 875 | }, 876 | "node_modules/normalize-path": { 877 | "version": "3.0.0", 878 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 879 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 880 | "dev": true, 881 | "engines": { 882 | "node": ">=0.10.0" 883 | } 884 | }, 885 | "node_modules/once": { 886 | "version": "1.4.0", 887 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 888 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 889 | "dev": true, 890 | "dependencies": { 891 | "wrappy": "1" 892 | } 893 | }, 894 | "node_modules/p-limit": { 895 | "version": "3.0.2", 896 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", 897 | "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", 898 | "dev": true, 899 | "dependencies": { 900 | "p-try": "^2.0.0" 901 | }, 902 | "engines": { 903 | "node": ">=10" 904 | }, 905 | "funding": { 906 | "url": "https://github.com/sponsors/sindresorhus" 907 | } 908 | }, 909 | "node_modules/p-locate": { 910 | "version": "5.0.0", 911 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 912 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 913 | "dev": true, 914 | "dependencies": { 915 | "p-limit": "^3.0.2" 916 | }, 917 | "engines": { 918 | "node": ">=10" 919 | }, 920 | "funding": { 921 | "url": "https://github.com/sponsors/sindresorhus" 922 | } 923 | }, 924 | "node_modules/p-try": { 925 | "version": "2.2.0", 926 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 927 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 928 | "dev": true, 929 | "engines": { 930 | "node": ">=6" 931 | } 932 | }, 933 | "node_modules/path-exists": { 934 | "version": "4.0.0", 935 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 936 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 937 | "dev": true, 938 | "engines": { 939 | "node": ">=8" 940 | } 941 | }, 942 | "node_modules/path-is-absolute": { 943 | "version": "1.0.1", 944 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 945 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 946 | "dev": true, 947 | "engines": { 948 | "node": ">=0.10.0" 949 | } 950 | }, 951 | "node_modules/pathval": { 952 | "version": "1.1.1", 953 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 954 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 955 | "dev": true, 956 | "engines": { 957 | "node": "*" 958 | } 959 | }, 960 | "node_modules/picomatch": { 961 | "version": "2.3.1", 962 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 963 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 964 | "dev": true, 965 | "engines": { 966 | "node": ">=8.6" 967 | }, 968 | "funding": { 969 | "url": "https://github.com/sponsors/jonschlinkert" 970 | } 971 | }, 972 | "node_modules/randombytes": { 973 | "version": "2.1.0", 974 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 975 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 976 | "dev": true, 977 | "dependencies": { 978 | "safe-buffer": "^5.1.0" 979 | } 980 | }, 981 | "node_modules/readdirp": { 982 | "version": "3.6.0", 983 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 984 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 985 | "dev": true, 986 | "dependencies": { 987 | "picomatch": "^2.2.1" 988 | }, 989 | "engines": { 990 | "node": ">=8.10.0" 991 | } 992 | }, 993 | "node_modules/require-directory": { 994 | "version": "2.1.1", 995 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 996 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 997 | "dev": true, 998 | "engines": { 999 | "node": ">=0.10.0" 1000 | } 1001 | }, 1002 | "node_modules/safe-buffer": { 1003 | "version": "5.2.1", 1004 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1005 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1006 | "dev": true, 1007 | "funding": [ 1008 | { 1009 | "type": "github", 1010 | "url": "https://github.com/sponsors/feross" 1011 | }, 1012 | { 1013 | "type": "patreon", 1014 | "url": "https://www.patreon.com/feross" 1015 | }, 1016 | { 1017 | "type": "consulting", 1018 | "url": "https://feross.org/support" 1019 | } 1020 | ] 1021 | }, 1022 | "node_modules/serialize-javascript": { 1023 | "version": "6.0.0", 1024 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1025 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1026 | "dev": true, 1027 | "dependencies": { 1028 | "randombytes": "^2.1.0" 1029 | } 1030 | }, 1031 | "node_modules/sort-any": { 1032 | "version": "2.0.0", 1033 | "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", 1034 | "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", 1035 | "dev": true, 1036 | "dependencies": { 1037 | "lodash": "^4.17.21" 1038 | } 1039 | }, 1040 | "node_modules/string-width": { 1041 | "version": "4.2.3", 1042 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1043 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1044 | "dev": true, 1045 | "dependencies": { 1046 | "emoji-regex": "^8.0.0", 1047 | "is-fullwidth-code-point": "^3.0.0", 1048 | "strip-ansi": "^6.0.1" 1049 | }, 1050 | "engines": { 1051 | "node": ">=8" 1052 | } 1053 | }, 1054 | "node_modules/strip-ansi": { 1055 | "version": "6.0.1", 1056 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1057 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1058 | "dev": true, 1059 | "dependencies": { 1060 | "ansi-regex": "^5.0.1" 1061 | }, 1062 | "engines": { 1063 | "node": ">=8" 1064 | } 1065 | }, 1066 | "node_modules/strip-json-comments": { 1067 | "version": "3.1.1", 1068 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1069 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1070 | "dev": true, 1071 | "engines": { 1072 | "node": ">=8" 1073 | }, 1074 | "funding": { 1075 | "url": "https://github.com/sponsors/sindresorhus" 1076 | } 1077 | }, 1078 | "node_modules/supports-color": { 1079 | "version": "8.1.1", 1080 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1081 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1082 | "dev": true, 1083 | "dependencies": { 1084 | "has-flag": "^4.0.0" 1085 | }, 1086 | "engines": { 1087 | "node": ">=10" 1088 | }, 1089 | "funding": { 1090 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1091 | } 1092 | }, 1093 | "node_modules/to-regex-range": { 1094 | "version": "5.0.1", 1095 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1096 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1097 | "dev": true, 1098 | "dependencies": { 1099 | "is-number": "^7.0.0" 1100 | }, 1101 | "engines": { 1102 | "node": ">=8.0" 1103 | } 1104 | }, 1105 | "node_modules/ts-node": { 1106 | "version": "10.9.2", 1107 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1108 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1109 | "dev": true, 1110 | "dependencies": { 1111 | "@cspotcode/source-map-support": "^0.8.0", 1112 | "@tsconfig/node10": "^1.0.7", 1113 | "@tsconfig/node12": "^1.0.7", 1114 | "@tsconfig/node14": "^1.0.0", 1115 | "@tsconfig/node16": "^1.0.2", 1116 | "acorn": "^8.4.1", 1117 | "acorn-walk": "^8.1.1", 1118 | "arg": "^4.1.0", 1119 | "create-require": "^1.1.0", 1120 | "diff": "^4.0.1", 1121 | "make-error": "^1.1.1", 1122 | "v8-compile-cache-lib": "^3.0.1", 1123 | "yn": "3.1.1" 1124 | }, 1125 | "bin": { 1126 | "ts-node": "dist/bin.js", 1127 | "ts-node-cwd": "dist/bin-cwd.js", 1128 | "ts-node-esm": "dist/bin-esm.js", 1129 | "ts-node-script": "dist/bin-script.js", 1130 | "ts-node-transpile-only": "dist/bin-transpile.js", 1131 | "ts-script": "dist/bin-script-deprecated.js" 1132 | }, 1133 | "peerDependencies": { 1134 | "@swc/core": ">=1.2.50", 1135 | "@swc/wasm": ">=1.2.50", 1136 | "@types/node": "*", 1137 | "typescript": ">=2.7" 1138 | }, 1139 | "peerDependenciesMeta": { 1140 | "@swc/core": { 1141 | "optional": true 1142 | }, 1143 | "@swc/wasm": { 1144 | "optional": true 1145 | } 1146 | } 1147 | }, 1148 | "node_modules/ts-node/node_modules/diff": { 1149 | "version": "4.0.2", 1150 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 1151 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 1152 | "dev": true, 1153 | "engines": { 1154 | "node": ">=0.3.1" 1155 | } 1156 | }, 1157 | "node_modules/type-detect": { 1158 | "version": "4.0.8", 1159 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1160 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1161 | "dev": true, 1162 | "engines": { 1163 | "node": ">=4" 1164 | } 1165 | }, 1166 | "node_modules/typescript": { 1167 | "version": "4.9.5", 1168 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", 1169 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", 1170 | "dev": true, 1171 | "bin": { 1172 | "tsc": "bin/tsc", 1173 | "tsserver": "bin/tsserver" 1174 | }, 1175 | "engines": { 1176 | "node": ">=4.2.0" 1177 | } 1178 | }, 1179 | "node_modules/undici-types": { 1180 | "version": "5.26.5", 1181 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1182 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 1183 | "dev": true 1184 | }, 1185 | "node_modules/v8-compile-cache-lib": { 1186 | "version": "3.0.1", 1187 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 1188 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 1189 | "dev": true 1190 | }, 1191 | "node_modules/which": { 1192 | "version": "2.0.2", 1193 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1194 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1195 | "dev": true, 1196 | "dependencies": { 1197 | "isexe": "^2.0.0" 1198 | }, 1199 | "bin": { 1200 | "node-which": "bin/node-which" 1201 | }, 1202 | "engines": { 1203 | "node": ">= 8" 1204 | } 1205 | }, 1206 | "node_modules/workerpool": { 1207 | "version": "6.2.0", 1208 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", 1209 | "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", 1210 | "dev": true 1211 | }, 1212 | "node_modules/wrap-ansi": { 1213 | "version": "7.0.0", 1214 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1215 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1216 | "dev": true, 1217 | "dependencies": { 1218 | "ansi-styles": "^4.0.0", 1219 | "string-width": "^4.1.0", 1220 | "strip-ansi": "^6.0.0" 1221 | }, 1222 | "engines": { 1223 | "node": ">=10" 1224 | }, 1225 | "funding": { 1226 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1227 | } 1228 | }, 1229 | "node_modules/wrappy": { 1230 | "version": "1.0.2", 1231 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1232 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1233 | "dev": true 1234 | }, 1235 | "node_modules/y18n": { 1236 | "version": "5.0.8", 1237 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1238 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1239 | "dev": true, 1240 | "engines": { 1241 | "node": ">=10" 1242 | } 1243 | }, 1244 | "node_modules/yargs": { 1245 | "version": "16.2.0", 1246 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1247 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1248 | "dev": true, 1249 | "dependencies": { 1250 | "cliui": "^7.0.2", 1251 | "escalade": "^3.1.1", 1252 | "get-caller-file": "^2.0.5", 1253 | "require-directory": "^2.1.1", 1254 | "string-width": "^4.2.0", 1255 | "y18n": "^5.0.5", 1256 | "yargs-parser": "^20.2.2" 1257 | }, 1258 | "engines": { 1259 | "node": ">=10" 1260 | } 1261 | }, 1262 | "node_modules/yargs-parser": { 1263 | "version": "20.2.4", 1264 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1265 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1266 | "dev": true, 1267 | "engines": { 1268 | "node": ">=10" 1269 | } 1270 | }, 1271 | "node_modules/yargs-unparser": { 1272 | "version": "2.0.0", 1273 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1274 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1275 | "dev": true, 1276 | "dependencies": { 1277 | "camelcase": "^6.0.0", 1278 | "decamelize": "^4.0.0", 1279 | "flat": "^5.0.2", 1280 | "is-plain-obj": "^2.1.0" 1281 | }, 1282 | "engines": { 1283 | "node": ">=10" 1284 | } 1285 | }, 1286 | "node_modules/yn": { 1287 | "version": "3.1.1", 1288 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1289 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1290 | "dev": true, 1291 | "engines": { 1292 | "node": ">=6" 1293 | } 1294 | } 1295 | } 1296 | } 1297 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structurizr-typescript", 3 | "version": "1.0.15", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "mocha -r ts-node/register 'test/index.ts'" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ChristianEder/structurizr-typescript.git" 14 | }, 15 | "author": "Christian Eder", 16 | "bugs": { 17 | "url": "https://github.com/ChristianEder/structurizr-typescript/issues" 18 | }, 19 | "homepage": "https://github.com/ChristianEder/structurizr-typescript#readme", 20 | "devDependencies": { 21 | "@types/chai": "^4.2.0", 22 | "@types/mocha": "^9.0.0", 23 | "@types/crypto-js": "4.2.2", 24 | "@types/node": "20.11.24", 25 | "deep-equal-in-any-order": "2.0.6", 26 | "@types/deep-equal-in-any-order": "1.0.3", 27 | "chai": "^4.3.0", 28 | "mocha": "^9.1.0", 29 | "ts-node": "^10.0.0", 30 | "typescript": "^4.0.0" 31 | }, 32 | "dependencies": { 33 | "crypto-js": "4.2.0" 34 | } 35 | } -------------------------------------------------------------------------------- /sample/exportPlantUML.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { Workspace, PlantUMLWriter } from "structurizr-typescript" 3 | 4 | export const exportPlantUML = (location: string, workspace: Workspace) => { 5 | const plantUML = new PlantUMLWriter().toPlantUML(workspace); 6 | fs.writeFileSync(location, plantUML); 7 | }; -------------------------------------------------------------------------------- /sample/index.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from './workspace'; 2 | import { pushWorkspace } from './pushWorkspace'; 3 | import { exportPlantUML } from './exportPlantUML'; 4 | 5 | const main = async () => { 6 | // Now either write the workspace to the Structurizr backend... 7 | const response = await pushWorkspace(workspace); 8 | if (response) { 9 | console.log('> workspace pushed to backend', response); 10 | } 11 | 12 | // ... or render it as PlantUML 13 | const location = 'plant.puml'; 14 | exportPlantUML(location, workspace); 15 | console.log('> workspace rendered as PlantUML at', location); 16 | }; 17 | 18 | main().catch((e) => console.error('error', e)); 19 | -------------------------------------------------------------------------------- /sample/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structurizr-typescript-sample", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "11.13.10", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.10.tgz", 10 | "integrity": "sha512-leUNzbFTMX94TWaIKz8N15Chu55F9QSH+INKayQr5xpkasBQBRF3qQXfo3/dOnMU/dEIit+Y/SU8HyOjq++GwA==", 11 | "dev": true 12 | }, 13 | "crypto-js": { 14 | "version": "3.1.9-1", 15 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", 16 | "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" 17 | }, 18 | "structurizr-typescript": { 19 | "version": "1.0.8", 20 | "resolved": "https://registry.npmjs.org/structurizr-typescript/-/structurizr-typescript-1.0.8.tgz", 21 | "integrity": "sha512-FQRw9k+LGmqLLoYRkoc1DUfA58eK+om1s5kgI88gGQacAYOpjRU4SQqTj/V6WU76akERv9UEhjebOIF1o9ziHA==", 22 | "requires": { 23 | "crypto-js": "3.1.9-1" 24 | } 25 | }, 26 | "typescript": { 27 | "version": "3.7.3", 28 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", 29 | "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", 30 | "dev": true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "structurizr-typescript-sample", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "tsc && node dist/index.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^11.13.2", 14 | "typescript": "3.7.3" 15 | }, 16 | "dependencies": { 17 | "structurizr-typescript": "1.0.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/pushWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { Workspace, StructurizrClient } from "structurizr-typescript" 2 | 3 | export const pushWorkspace = (workspace: Workspace) => { 4 | if (!process.env.WORKSPACE_ID) { 5 | return console.error('Please define WORKSPACE_ID environment variable in order to push your workspace'); 6 | } 7 | 8 | if (!process.env.API_KEY || !process.env.API_SECRET) { 9 | return console.error('Please define API_KEY and API_SECRET environment variables in order to push your workspace'); 10 | } 11 | 12 | const workspaceId = parseInt(process.env.WORKSPACE_ID); 13 | 14 | const client = new StructurizrClient(process.env.API_KEY, process.env.API_SECRET); 15 | 16 | return client.putWorkspace(workspaceId, workspace); 17 | } -------------------------------------------------------------------------------- /sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true 8 | }, 9 | "exclude": [ 10 | "dist" 11 | ] 12 | } -------------------------------------------------------------------------------- /sample/workspace.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Workspace, 3 | Location, 4 | InteractionStyle, 5 | } from 'structurizr-typescript'; 6 | 7 | export const workspace = new Workspace("some workspace name", "some description"); 8 | workspace.name = 'Monkey Factory'; 9 | 10 | const user = workspace.model.addPerson('User', 'uses the system')!; 11 | 12 | const admin = workspace.model.addPerson('Admin', 'administers the system and manages user')!; 13 | 14 | admin!.interactsWith(user!, 'manages rights'); 15 | 16 | const factory = workspace.model.addSoftwareSystem( 17 | 'Monkey Factory', 18 | 'Oversees the production of stuffed monkey animals' 19 | )!; 20 | factory.location = Location.Internal; 21 | 22 | const ingress = factory.addContainer('ingress', 'accepts incoming telemetry data', 'IoT Hub')!; 23 | const storage = factory.addContainer('storage', 'stores telemetry data', 'Table Storage')!; 24 | const frontend = factory.addContainer('frontend', 'visualizes telemetry data', 'React')!; 25 | ingress.uses(storage, 'store telemetry', 'IoT Hub routing'); 26 | frontend.uses(storage, 'load telemetry data', 'Table Storage SDK'); 27 | 28 | const crm = workspace.model.addSoftwareSystem('CRM system', 'manage tickets')!; 29 | crm.location = Location.External; 30 | factory.uses(crm, 'Create tickets', 'AMQP', InteractionStyle.Asynchronous); 31 | 32 | user.uses(factory, 'view dashboards'); 33 | admin.uses(factory, 'configure users'); 34 | admin.uses(crm, 'work on tickets'); 35 | 36 | const systemContext = workspace.views.createSystemContextView( 37 | factory, 38 | 'factory-context', 39 | 'The system context view for the monkey factory' 40 | ); 41 | systemContext.addNearestNeighbours(factory); 42 | 43 | const containerView = workspace.views.createContainerView( 44 | factory, 45 | 'factory-containers', 46 | 'Container view for the monkey factory' 47 | ); 48 | containerView.addAllContainers(); 49 | containerView.addNearestNeighbours(factory); -------------------------------------------------------------------------------- /src/client/api/hmac.ts: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from "crypto-js"; 2 | 3 | export function getMd5HmacBase64(content: string): string { 4 | var bytes = CryptoJS.enc.Utf8.parse(content).toString(CryptoJS.enc.Base64); 5 | var md5 = CryptoJS.MD5(content).toString(CryptoJS.enc.Base64); 6 | var digest = CryptoJS.enc.Base64.parse(md5).toString(); 7 | 8 | var contentMd5Base64Encoded = CryptoJS.enc.Utf8.parse(digest).toString(CryptoJS.enc.Base64); 9 | return contentMd5Base64Encoded; 10 | } -------------------------------------------------------------------------------- /src/client/api/httpClient.ts: -------------------------------------------------------------------------------- 1 | import * as https from "https"; 2 | 3 | export class HttpClient { 4 | 5 | constructor(private baseUri: string) { 6 | } 7 | 8 | public get(path: string, additionalHeaders?: {}): HttpResponse { 9 | return new HttpResponse(this.getRequestOptions(path, "GET", additionalHeaders)); 10 | } 11 | 12 | public put(path: string, content: {}, additionalHeaders?: {}): HttpResponse { 13 | return new HttpResponse(this.getRequestOptions(path, "PUT", additionalHeaders), content); 14 | } 15 | 16 | private getRequestOptions(path: string, method: string, additionalHeaders?: {}): https.RequestOptions { 17 | 18 | var headers = additionalHeaders || {}; 19 | return { 20 | host: this.baseUri, 21 | protocol: 'https:', 22 | port: 443, 23 | path: path, 24 | method: method, 25 | headers: headers 26 | }; 27 | } 28 | } 29 | 30 | export class HttpResponse { 31 | 32 | private data: string = ""; 33 | private promise!: Promise; 34 | 35 | constructor(options: https.RequestOptions, content?: any) { 36 | 37 | this.promise = new Promise((resolve, reject) => { 38 | 39 | var request = https.request(options, r => { 40 | r.on('data', chunk => { this.data += chunk; }); 41 | r.on('end', () => { 42 | resolve(this.data); 43 | }); 44 | r.on('error', (e) => { 45 | reject(e); 46 | }); 47 | }); 48 | 49 | if (content) { 50 | request.write(content); 51 | } 52 | request.end(); 53 | }); 54 | } 55 | 56 | public get done(): Promise { 57 | return this.promise; 58 | }; 59 | 60 | public get responseBody(): string { 61 | return this.data; 62 | } 63 | } -------------------------------------------------------------------------------- /src/client/api/structurizrClient.ts: -------------------------------------------------------------------------------- 1 | import { Workspace } from "../../core/model/workspace"; 2 | import * as CryptoJS from "crypto-js"; 3 | import { HttpClient } from "./httpClient"; 4 | 5 | export class StructurizrClient { 6 | 7 | private httpClient!: HttpClient; 8 | public mergeFromRemote = true; 9 | public verbose = false; 10 | 11 | constructor(private apiKey: string, private apiSecret: string, private url = "api.structurizr.com") { 12 | this.httpClient = new HttpClient(url); 13 | } 14 | 15 | public getWorkspace(workspaceId: number): Promise { 16 | var nonce = Date.now() + ""; 17 | var md5Digest = this.getMD5digest(""); 18 | var response = this.httpClient.get("/workspace/" + workspaceId, this.headers(workspaceId, "GET", md5Digest, nonce)); 19 | return response.done.then(j => { 20 | 21 | var dto = JSON.parse(j); 22 | if (!dto || dto["success"] != undefined && !dto["success"]) { 23 | throw new Error("Response from API seems to indicate an error: " + j); 24 | } 25 | 26 | var w = new Workspace("", ""); 27 | w.fromDto(JSON.parse(j)); 28 | w.hydrate(); 29 | return w; 30 | }) 31 | } 32 | 33 | public async putWorkspace(workspaceId: number, workspace: Workspace): Promise { 34 | 35 | if (this.mergeFromRemote) { 36 | this.log("Starting to get remote workspace for layout merging"); 37 | var remoteWorkspace = await this.getWorkspace(workspaceId); 38 | this.log("Gott remote workspace for layout merging"); 39 | workspace.views.copyLayoutInformationFrom(remoteWorkspace.views); 40 | this.log("Merged layout with remote workspace"); 41 | } 42 | 43 | workspace.id = workspaceId; 44 | workspace.lastModifiedDate = new Date(); 45 | const dto =workspace.toDto(); 46 | const json = JSON.stringify(dto); 47 | 48 | this.log("Serialized workspace:"); 49 | this.log(json); 50 | 51 | var nonce = Date.now() + ""; 52 | var md5Digest = this.getMD5digest(json); 53 | return this.httpClient.put("/workspace/" + workspaceId, json, this.headers(workspaceId, "PUT", md5Digest, nonce, json)).done; 54 | } 55 | 56 | private headers(workspaceId: number, method: "GET" | "PUT", md5Digest: string, nonce: string, json?: string): any { 57 | var headers: any = { 58 | "X-Authorization": this.getAuthorizationHeader(method, workspaceId, md5Digest, nonce, json), 59 | "User-Agent": "structurizr-typescript/0.0.3", 60 | "Nonce": nonce 61 | }; 62 | 63 | if (json) { 64 | headers["Content-Type"] = "application/json; charset=UTF-8"; 65 | headers["Content-MD5"] = this.toBase64EncodedUTF8(md5Digest); 66 | } 67 | 68 | return headers; 69 | } 70 | 71 | private getAuthorizationHeader(method: "PUT" | "GET", workspaceId: number, md5Digest: string, nonce: string, json?: string, ) { 72 | var hmac = this.gethmac(method, "/workspace/" + workspaceId, md5Digest, json ? "application/json; charset=UTF-8" : "", nonce); 73 | var authHeader = this.apiKey + ":" + this.toBase64EncodedUTF8(hmac); 74 | return authHeader; 75 | } 76 | 77 | private gethmac(method: string, path: string, md5: string, contentType: string, nonce: string): string { 78 | var content = method + "\n" + path + "\n" + md5 + "\n" + contentType + "\n" + nonce + "\n"; 79 | var contentBytes = CryptoJS.enc.Utf8.parse(content); 80 | var apiSecretBytes = CryptoJS.enc.Utf8.parse(this.apiSecret); 81 | var hash = CryptoJS.HmacSHA256(contentBytes, apiSecretBytes); 82 | var hmac = hash.toString(); 83 | return hmac; 84 | } 85 | 86 | private getMD5digest(content: string) { 87 | var md5 = CryptoJS.MD5(content).toString(CryptoJS.enc.Base64); 88 | var digest = CryptoJS.enc.Base64.parse(md5).toString(); 89 | return digest; 90 | } 91 | 92 | private toBase64EncodedUTF8(text: string): string { 93 | return CryptoJS.enc.Utf8.parse(text).toString(CryptoJS.enc.Base64); 94 | } 95 | 96 | private log(message: any){ 97 | if(this.verbose){ 98 | console.log(message); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api/structurizrClient'; 2 | export * from './plantUML/plantUMLWriter'; 3 | -------------------------------------------------------------------------------- /src/client/plantUML/plantUMLWriter.ts: -------------------------------------------------------------------------------- 1 | import { Workspace, SystemContextView, ContainerView, DeploymentView, View, Element, RelationshipView, Person, SoftwareSystem, Container, Relationship, ComponentView, Component, StaticView, StaticStructureElement } from "../../core"; 2 | import { DeploymentNode } from "../../core/model/deploymentNode"; 3 | import { ContainerInstance } from "../../core/model/containerInstance"; 4 | 5 | class StringWriter { 6 | private value = ""; 7 | 8 | public write(content: string): void { 9 | this.value += content; 10 | } 11 | 12 | public writeLine(content: string): void { 13 | this.write(content); 14 | this.newline(); 15 | } 16 | 17 | public newline(): void { 18 | this.write("\r\n"); 19 | } 20 | 21 | public toString(): string { 22 | return this.value; 23 | } 24 | } 25 | 26 | export class PlantUMLWriter { 27 | public toPlantUML(workspace: Workspace): string { 28 | let result = new StringWriter(); 29 | 30 | if (workspace) { 31 | workspace.views.systemContextViews.forEach(v => { 32 | this.writeSystemContextView(v, result); 33 | }); 34 | workspace.views.containerViews.forEach(v => { 35 | this.writeContainerView(v, result); 36 | }); 37 | workspace.views.componentViews.forEach(v => { 38 | this.writeComponentView(v, result); 39 | }); 40 | workspace.views.deploymentViews.forEach(v => { 41 | this.writeDeploymentView(v, result); 42 | }); 43 | } 44 | 45 | return result.toString(); 46 | } 47 | 48 | private writeSystemContextView(v: SystemContextView, writer: StringWriter) { 49 | this.writeHeader(v, writer); 50 | 51 | v.elements 52 | .map(e => e.element) 53 | .sort(this.by(e => e.name)) 54 | .forEach(e => this.writeElement(e, writer, false)); 55 | 56 | this.writeRelationships(v.relationships, writer); 57 | 58 | this.writeFooter(writer); 59 | } 60 | 61 | private writeContainerView(v: ContainerView, writer: StringWriter) { 62 | this.writeStaticView(v, Container.type, v.softwareSystem!, writer); 63 | } 64 | 65 | private writeComponentView(v: ComponentView, writer: StringWriter) { 66 | this.writeStaticView(v, Component.type, v.container!, writer); 67 | } 68 | 69 | private writeStaticView(v: StaticView, type: string, element: StaticStructureElement, writer: StringWriter){ 70 | this.writeHeader(v, writer); 71 | 72 | v.elements 73 | .map(e => e.element) 74 | .filter(e => e.type !== type) 75 | .sort(this.by(e => e.name)) 76 | .forEach(e => this.writeElement(e, writer, false)); 77 | 78 | writer.writeLine("package " + this.nameOf(element!.name) + " {"); 79 | 80 | v.elements 81 | .map(e => e.element) 82 | .filter(e => e.type === type) 83 | .sort(this.by(e => e.name)) 84 | .forEach(e => this.writeElement(e, writer, true)); 85 | 86 | writer.writeLine("}"); 87 | 88 | this.writeRelationships(v.relationships, writer); 89 | 90 | this.writeFooter(writer); 91 | } 92 | 93 | private writeDeploymentView(v: DeploymentView, writer: StringWriter) { 94 | this.writeHeader(v, writer); 95 | 96 | v.elements 97 | .filter(e => e.element.type === DeploymentNode.type && !e.element.parent) 98 | .map(e => e.element) 99 | .sort(this.by(e => e.name)) 100 | .forEach(e => this.writeDeploymentNode(e, writer, 0)); 101 | 102 | this.writeRelationships(v.relationships, writer); 103 | 104 | this.writeFooter(writer); 105 | } 106 | 107 | private writeDeploymentNode(e: DeploymentNode, writer: StringWriter, indent: number): void { 108 | 109 | writer.writeLine(`${" ".repeat(indent)}node \"${e.name + (e.instances > 1 ? " (x" + e.instances + ")" : "")}\" <<${this.typeOf(e)}>> as ${e.id} {`); 110 | 111 | e.children.forEach(d => this.writeDeploymentNode(d, writer, indent + 1)); 112 | 113 | e.containerInstances.forEach(i => this.writeContainerInstance(i, writer, indent + 1)); 114 | 115 | writer.writeLine(" ".repeat(indent) + "}"); 116 | } 117 | 118 | private writeContainerInstance(i: ContainerInstance, writer: StringWriter, indent: number): void { 119 | writer.writeLine(`${" ".repeat(indent)}artifact \"${i.container!.name}\" <<${this.typeOf(i)}>> as ${i.id}`); 120 | } 121 | 122 | private writeElement(e: Element, writer: StringWriter, indent: boolean): void { 123 | writer.writeLine(`${(indent ? " " : "")}${(e.type === Person.type ? "actor" : "component")} \"${e.name}\" <<${this.typeOf(e)}>> as ${e.id}`); 124 | } 125 | 126 | writeRelationships(relationships: RelationshipView[], writer: StringWriter) { 127 | relationships.map(r => r.relationship) 128 | .sort(this.by(r => r.source.name + r.destination.name)) 129 | .forEach(r => this.writeRelationship(r, writer)); 130 | } 131 | 132 | writeRelationship(r: Relationship, writer: StringWriter) { 133 | writer.writeLine(`${r.source.id} ..> ${r.destination.id} ${(r.description && r.description.length ? ": " + r.description : "")}${(r.technology && r.technology.length ? " <<" + r.technology + ">>" : "")}`); 134 | } 135 | 136 | private writeHeader(v: View, writer: StringWriter) { 137 | writer.write("@startuml"); 138 | writer.newline(); 139 | 140 | writer.write("title " + v.name); 141 | writer.newline(); 142 | 143 | if (v.description) { 144 | writer.write("caption " + v.description); 145 | writer.newline(); 146 | } 147 | } 148 | 149 | private by(value: (i: TItem) => TProperty): (a: TItem, b: TItem) => number { 150 | return (a, b) => { 151 | var va = value(a); 152 | var vb = value(b); 153 | return va > vb ? 1 : va < vb ? -1 : 0; 154 | }; 155 | } 156 | 157 | private typeOf(e: Element): string { 158 | if (e.type === Person.type) { 159 | return "Person"; 160 | } 161 | 162 | if (e.type === SoftwareSystem.type) { 163 | return "Software System"; 164 | } 165 | 166 | if (e.type === Container.type) { 167 | return "Container"; 168 | } 169 | 170 | if (e.type === DeploymentNode.type) { 171 | const deploymentNode = e; 172 | return deploymentNode.technology && deploymentNode.technology.length ? deploymentNode.technology : "Deployment Node"; 173 | } 174 | 175 | if (e.type === ContainerInstance.type) { 176 | return "Container"; 177 | } 178 | 179 | return ""; 180 | } 181 | 182 | private nameOf(s: string): string { 183 | return s ? `"${s}"` : ""; 184 | } 185 | 186 | private writeFooter(writer: StringWriter) { 187 | writer.write("@enduml"); 188 | writer.newline(); 189 | writer.newline(); 190 | } 191 | } -------------------------------------------------------------------------------- /src/core/documentation/decision.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "../model/element"; 2 | import { Format } from "./format"; 3 | import { DecisionStatus } from "./decisionStatus"; 4 | 5 | export class Decision { 6 | public id?: string; 7 | public date?: Date; 8 | public status?: DecisionStatus; 9 | public element?: Element; 10 | public title?: string; 11 | public format?: Format; 12 | public content?: string; 13 | 14 | private _elementId?: string; 15 | 16 | get elementId() { 17 | return this.element ? this.element.id : this._elementId; 18 | } 19 | 20 | set elementId(value) { 21 | this._elementId = value; 22 | } 23 | 24 | public toDto() { 25 | return { 26 | id: this.id, 27 | date: this.date!.toISOString(), 28 | status: this.status, 29 | title: this.title, 30 | content: this.content, 31 | format: this.format, 32 | elementId: this.elementId 33 | }; 34 | } 35 | 36 | public fromDto(dto: any) { 37 | this.id = dto.id; 38 | this.date = new Date(dto.date); 39 | this.status = dto.status; 40 | this.title = dto.title; 41 | this.content = dto.content; 42 | this.format = dto.format; 43 | this.elementId = dto.elementId; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/documentation/decisionStatus.ts: -------------------------------------------------------------------------------- 1 | export enum DecisionStatus { 2 | Proposed = "Proposed", 3 | Accepted = "Accepted", 4 | Superseded = "Superseded", 5 | Deprecated = "Deprecated", 6 | Rejected = "Rejected" 7 | } 8 | -------------------------------------------------------------------------------- /src/core/documentation/documentation.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../model/model"; 2 | import { Element } from "../model/element"; 3 | import assert = require('assert'); 4 | import { Section } from "./section"; 5 | import { Decision } from "./decision"; 6 | import { SoftwareSystem } from "../model/softwareSystem"; 7 | import { DecisionStatus } from "./decisionStatus"; 8 | import { Format } from "./format"; 9 | 10 | export class Documentation { 11 | private model: Model; 12 | public sections: Set
= new Set(); 13 | public decisions: Set = new Set(); 14 | 15 | constructor(model: Model) { 16 | this.model = model; 17 | } 18 | 19 | public addSection( 20 | element: Element | undefined, 21 | title: string, 22 | format: any, 23 | content: string 24 | ) { 25 | if (element && !this.model.containsElement(element)) { 26 | throw new Error( 27 | `The element named ${element.name} does not exist in the model associated with this documentation.` 28 | ); 29 | } 30 | 31 | assert(title, "A title must be specified."); 32 | assert(content, "Content must be specified."); 33 | assert(format, "A format must be specified."); 34 | this.checkSectionIsUnqiue(title, element); 35 | 36 | const section = new Section(); 37 | section.fromDto({ 38 | elementId: element && element.id, 39 | title, 40 | order: this.sections.size + 1, 41 | format, 42 | content 43 | }); 44 | section.element = element; 45 | this.sections.add(section); 46 | return section; 47 | } 48 | 49 | public addDecision( 50 | element: SoftwareSystem | undefined, 51 | id: string, 52 | date: Date, 53 | title: string, 54 | status: DecisionStatus, 55 | format: Format, 56 | content: string 57 | ) { 58 | assert(id, `An id must be specifed`); 59 | assert(date, "A date must be specified"); 60 | assert(title, "A title must be specified"); 61 | assert(status, "A status must be specified"); 62 | assert(format, "A format must be specified"); 63 | this.checkDecisionIsUnqiue(id, element); 64 | 65 | const decision = new Decision(); 66 | decision.fromDto({ id, date, title, status, format, content, elementId: element && element.id }); 67 | decision.element = element; 68 | this.decisions.add(decision); 69 | return decision; 70 | } 71 | 72 | public toDto() { 73 | return { 74 | sections: Array.from(this.sections.values()).map(section => 75 | section.toDto() 76 | ), 77 | decisions: Array.from(this.decisions.values()).map(decision => 78 | decision.toDto() 79 | ) 80 | }; 81 | } 82 | 83 | public fromDto(dto: any) { 84 | if (dto.sections) { 85 | dto.sections.forEach((sectionDto: any) => { 86 | const section = new Section(); 87 | section.fromDto(sectionDto); 88 | this.sections.add(section); 89 | }); 90 | } 91 | if (dto.decisions) { 92 | dto.decisions.forEach((decisionDto: any) => { 93 | const decision = new Decision(); 94 | decision.fromDto(decisionDto); 95 | this.decisions.add(decision); 96 | }); 97 | } 98 | } 99 | 100 | public hydrate() { 101 | this.sections.forEach(section => this.findElementAndHydrate(section)); 102 | this.decisions.forEach(decision => 103 | this.findElementAndHydrate(decision) 104 | ); 105 | } 106 | 107 | private findElementAndHydrate(documentationElement: { 108 | elementId?: string; 109 | element?: Element; 110 | }) { 111 | if (documentationElement.elementId) { 112 | documentationElement.element = this.model.getElement( 113 | documentationElement.elementId 114 | ); 115 | } 116 | } 117 | 118 | private checkSectionIsUnqiue(title: string, element?: Element) { 119 | if (!element) { 120 | this.sections.forEach(section => { 121 | if (!section.element && section.title === title) { 122 | throw new Error( 123 | `A section with a title of ${title} already exists for this workspace.` 124 | ); 125 | } 126 | }); 127 | } else { 128 | this.sections.forEach(section => { 129 | if ( 130 | element.id === section.elementId && 131 | section.title === title 132 | ) { 133 | throw new Error( 134 | `A section with a title of ${title} already exists for element named ${element.name}.` 135 | ); 136 | } 137 | }); 138 | } 139 | } 140 | 141 | private checkDecisionIsUnqiue(id: string, element?: Element) { 142 | if (!element) { 143 | this.decisions.forEach(decision => { 144 | if (!decision.element && decision.id === id) { 145 | throw new Error( 146 | `A decision with an id of ${id} already exists for this workspace.` 147 | ); 148 | } 149 | }); 150 | } else { 151 | this.decisions.forEach(decision => { 152 | if (element.id === decision.elementId && decision.id === id) { 153 | throw new Error( 154 | `A decision with an title of ${id} already exists for element named ${element.name}.` 155 | ); 156 | } 157 | }); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/core/documentation/format.ts: -------------------------------------------------------------------------------- 1 | export enum Format { 2 | Markdown = "Markdown", 3 | AsciiDoc = "AsciiDoc" 4 | } 5 | -------------------------------------------------------------------------------- /src/core/documentation/section.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "../model/element"; 2 | import { Format } from "./format"; 3 | 4 | export class Section { 5 | public element?: Element; 6 | public title?: string; 7 | public order?: number; 8 | public format?: Format; 9 | public content?: string; 10 | 11 | private _elementId?: string; 12 | get elementId() { 13 | return this.element ? this.element.id : this._elementId; 14 | } 15 | set elementId(value) { 16 | this._elementId = value; 17 | } 18 | 19 | public toDto() { 20 | return { 21 | title: this.title, 22 | content: this.content, 23 | format: this.format, 24 | order: this.order, 25 | elementId: this.element ? this.element.id : null 26 | }; 27 | } 28 | 29 | public fromDto(dto: any) { 30 | this.title = dto.title; 31 | this.content = dto.content; 32 | this.format = dto.format; 33 | this.order = dto.order; 34 | this.elementId = dto.elementId; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./model/abstractImpliedRelationshipsStrategy"; 2 | export * from "./model/codeElement"; 3 | export * from "./model/component"; 4 | export * from "./model/container"; 5 | export * from "./model/containerInstance"; 6 | export * from "./model/createImpliedRelationshipsUnlessAnyRelationshipExistsStrategy"; 7 | export * from "./model/createImpliedRelationshipsUnlessSameRelationshipExistsStrategy"; 8 | export * from "./model/defaultImpliedRelationshipsStrategy"; 9 | export * from "./model/deploymentElement"; 10 | export * from "./model/deploymentNode"; 11 | export * from "./model/element"; 12 | export * from "./model/httpHealthCheck"; 13 | export * from "./model/iequatable"; 14 | export * from "./model/impliedRelationshipsStrategy"; 15 | export * from "./model/interactionStyle"; 16 | export * from "./model/isChildOf"; 17 | export * from "./model/location"; 18 | export * from "./model/model"; 19 | export * from "./model/modelItem"; 20 | export * from "./model/person"; 21 | export * from "./model/relationship"; 22 | export * from "./model/relationships"; 23 | export * from "./model/role"; 24 | export * from "./model/softwareSystem"; 25 | export * from "./model/staticStructureElement"; 26 | export * from "./model/tags"; 27 | export * from "./model/user"; 28 | export * from "./model/workspace"; 29 | export * from "./model/workspaceConfiguration"; 30 | 31 | export * from "./view/automaticLayout"; 32 | export * from "./view/branding"; 33 | export * from "./view/componentView"; 34 | export * from "./view/containerView"; 35 | export * from "./view/deploymentView"; 36 | export * from "./view/elementView"; 37 | export * from "./view/filteredView"; 38 | export * from "./view/paperSize"; 39 | export * from "./view/rankDirection"; 40 | export * from "./view/relationshipView"; 41 | export * from "./view/staticView"; 42 | export * from "./view/styles"; 43 | export * from "./view/systemContextView"; 44 | export * from "./view/terminology"; 45 | export * from "./view/view"; 46 | export * from "./view/viewConfiguration"; 47 | export * from "./view/viewSet"; 48 | 49 | export * from "./documentation/documentation"; 50 | export * from "./documentation/format"; 51 | export * from "./documentation/section"; 52 | export * from "./documentation/decision"; 53 | export * from "./documentation/decisionStatus"; 54 | -------------------------------------------------------------------------------- /src/core/model/abstractImpliedRelationshipsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Element, isChildOf, Relationship, IImpliedRelationshipsStrategy } from ".."; 2 | 3 | export abstract class AbstractImpliedRelationshipsStrategy implements IImpliedRelationshipsStrategy 4 | { 5 | protected impliedRelationshipIsAllowed(source: Element, destination: Element): boolean 6 | { 7 | if (source.equals(destination)) 8 | { 9 | return false; 10 | } 11 | 12 | return !(isChildOf(source, destination) || isChildOf(destination, source)); 13 | } 14 | 15 | 16 | abstract createImpliedRelationships(relationship: Relationship) : void; 17 | } -------------------------------------------------------------------------------- /src/core/model/codeElement.ts: -------------------------------------------------------------------------------- 1 | import { IEquatable } from "./iequatable"; 2 | 3 | export enum CodeElementRole 4 | { 5 | Primary = "Primary", 6 | Supporting = "Supporting" 7 | } 8 | 9 | export class CodeElement implements IEquatable{ 10 | public role: CodeElementRole = CodeElementRole.Primary; 11 | public name?: string; 12 | public type?: string; 13 | public description?: string; 14 | public url?: string; 15 | public language?: string; 16 | public category?: string; 17 | public visibility?: string; 18 | public size: number = 0; 19 | 20 | public toDto(): any { 21 | return JSON.parse(JSON.stringify(this)); 22 | } 23 | 24 | public fromDto(dto: any): CodeElement { 25 | const self: any = this; 26 | for (let field in dto) { 27 | self[field] = dto[field]; 28 | } 29 | return this; 30 | } 31 | 32 | public equals(other: CodeElement): boolean { 33 | if (!other) { 34 | return false; 35 | } 36 | 37 | if (other === this) { 38 | return true; 39 | } 40 | 41 | return this.type === other.type; 42 | } 43 | } -------------------------------------------------------------------------------- /src/core/model/component.ts: -------------------------------------------------------------------------------- 1 | import { StaticStructureElement } from "./staticStructureElement"; 2 | import { IEquatable } from "./iequatable"; 3 | import { Container } from "./container"; 4 | import { CodeElement, CodeElementRole } from "./codeElement"; 5 | import { Element } from "./element"; 6 | import { Tags } from "./tags"; 7 | 8 | export class Component extends StaticStructureElement implements IEquatable{ 9 | 10 | public static type = "Component"; 11 | public get type(): string { return Component.type; } 12 | 13 | public parent!: Element | null; 14 | 15 | public technology?: string; 16 | public size: number = 0; 17 | public codeElements: CodeElement[] = []; 18 | 19 | public get primaryCodeElement(): CodeElement | null { 20 | return this.codeElements.find(c => c.role === CodeElementRole.Primary) || null; 21 | } 22 | 23 | public get primaryType(): string | null { 24 | return this.primaryCodeElement?.type || null; 25 | } 26 | 27 | public set primaryType(value: string | null) { 28 | 29 | if (value === this.primaryType) { 30 | return; 31 | } 32 | 33 | this.codeElements = this.codeElements.filter(c => c.role !== CodeElementRole.Primary); 34 | 35 | if (value && value.trim().length) { 36 | const primaryCodeElement = new CodeElement(); 37 | primaryCodeElement.type = value; 38 | primaryCodeElement.name = value; 39 | primaryCodeElement.role = CodeElementRole.Primary; 40 | this.codeElements.push(primaryCodeElement); 41 | } 42 | } 43 | 44 | public addSupportingType(type: string, name?: string): CodeElement { 45 | const codeElement = new CodeElement(); 46 | codeElement.type = type; 47 | codeElement.name = name || type; 48 | codeElement.role = CodeElementRole.Supporting; 49 | 50 | if (!this.codeElements.some(c => c.equals(codeElement))) { 51 | this.codeElements.push(codeElement); 52 | } 53 | return codeElement; 54 | } 55 | 56 | public get container(): Container | null { 57 | return this.parent as Container; 58 | } 59 | 60 | public get canonicalName(): string { 61 | return this.parent!.canonicalName + Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name); 62 | } 63 | 64 | public toDto() { 65 | var dto = super.toDto(); 66 | dto.technology = this.technology; 67 | dto.size = this.size; 68 | dto.code = this.codeElements.map(c => c.toDto()); 69 | return dto; 70 | } 71 | 72 | public fromDto(dto: any) { 73 | super.fromDto(dto); 74 | this.technology = dto.technology; 75 | this.size = dto.size; 76 | this.codeElements = dto.code ? dto.code.map((c: any) => new CodeElement().fromDto(c)) : []; 77 | } 78 | 79 | public getRequiredTags() { 80 | return [Tags.Element, Tags.Component]; 81 | } 82 | } -------------------------------------------------------------------------------- /src/core/model/container.ts: -------------------------------------------------------------------------------- 1 | import { StaticStructureElement } from "./staticStructureElement"; 2 | import { IEquatable } from "./iequatable"; 3 | import { SoftwareSystem } from "./softwareSystem"; 4 | import { Element } from "./element"; 5 | import { Tags } from "./tags"; 6 | import { Component } from "./component"; 7 | 8 | export class Container extends StaticStructureElement implements IEquatable{ 9 | 10 | public static type = "Container"; 11 | public get type(): string { return Container.type; } 12 | 13 | public parent!: Element | null; 14 | 15 | public technology?: string; 16 | 17 | public components: Component[] = []; 18 | 19 | public get softwareSystem(): SoftwareSystem | null { 20 | return this.parent as SoftwareSystem; 21 | } 22 | 23 | public get canonicalName(): string { 24 | return this.parent!.canonicalName + Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name); 25 | } 26 | 27 | public addComponent(name: string, description: string, type?: string, technology?: string): Component | null { 28 | return this.model.addComponent(this, name, description, type, technology); 29 | } 30 | 31 | public toDto() { 32 | var dto = super.toDto(); 33 | dto.technology = this.technology; 34 | dto.components = this.components.map(c => c.toDto()); 35 | return dto; 36 | } 37 | 38 | public fromDto(dto: any) { 39 | super.fromDto(dto); 40 | this.technology = dto.technology; 41 | this.components = dto.components ? dto.components.map((cd: any) => { 42 | var c = new Component(); 43 | c.fromDto(cd); 44 | c.parent = this; 45 | return c; 46 | }) : [] 47 | } 48 | 49 | public getRequiredTags() { 50 | return [Tags.Element, Tags.Container]; 51 | } 52 | } -------------------------------------------------------------------------------- /src/core/model/containerInstance.ts: -------------------------------------------------------------------------------- 1 | import { DeploymentElement } from "./deploymentElement"; 2 | import { Container } from "./container"; 3 | import { Element } from "./element"; 4 | import { HttpHealthCheck } from "./httpHealthCheck"; 5 | import { Tags } from "./tags"; 6 | 7 | export class ContainerInstance extends DeploymentElement { 8 | 9 | public static type = "ContainerInstance"; 10 | public get type(): string { return ContainerInstance.type; } 11 | 12 | 13 | constructor(){ 14 | super(); 15 | this.tags.add(Tags.ContainerInstance); 16 | } 17 | 18 | public get canonicalName(): string { 19 | return this.container!.canonicalName + "[" + this.instanceId + "]" 20 | } 21 | 22 | public get parent(): Element | null { 23 | return this.container!.parent; 24 | } 25 | public set parent(p: Element | null) { 26 | } 27 | 28 | public instanceId!: number; 29 | 30 | public containerId?: string; 31 | public container?: Container; 32 | public healthChecks: HttpHealthCheck[] = []; 33 | 34 | public addHealthCheck(name: string, url: string, interval: number = 60, timeout: number = 0) { 35 | var healthCheck = new HttpHealthCheck(); 36 | healthCheck.name = name; 37 | healthCheck.url = url; 38 | healthCheck.interval = interval; 39 | healthCheck.timeout = timeout; 40 | if (!this.healthChecks.find(h => h.equals(healthCheck))) { 41 | this.healthChecks.push(healthCheck); 42 | } 43 | } 44 | 45 | public toDto(): any { 46 | var dto = super.toDto(); 47 | return { 48 | ...dto, 49 | environment: this.environment, 50 | containerId: this.containerId, 51 | instanceId: this.instanceId, 52 | healthChecks: this.healthChecks.map(h => h.toDto()) 53 | }; 54 | } 55 | 56 | public fromDto(dto: any) { 57 | super.fromDto(dto); 58 | this.environment = dto.environment; 59 | this.containerId = dto.containerId; 60 | this.instanceId = dto.instanceId; 61 | this.healthChecks = dto.healthChecks ? dto.healthChecks.map((h: any) => { 62 | var c = new HttpHealthCheck(); 63 | c.fromDto(h); 64 | return c; 65 | }) : []; 66 | } 67 | } -------------------------------------------------------------------------------- /src/core/model/createImpliedRelationshipsUnlessAnyRelationshipExistsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Relationship, Element, AbstractImpliedRelationshipsStrategy } from '..'; 2 | 3 | export class CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy extends AbstractImpliedRelationshipsStrategy { 4 | 5 | createImpliedRelationships(relationship: Relationship): void { 6 | let source: Element | null = relationship.source; 7 | let destination: Element | null = relationship.destination; 8 | 9 | const model = source.model; 10 | 11 | while (source) { 12 | while (destination) { 13 | if (this.impliedRelationshipIsAllowed(source, destination)) { 14 | const createRelationship = !source.relationships.getEfferentRelationshipWith(destination); 15 | 16 | if (createRelationship) { 17 | const newRelationship = model.addRelationship(source, destination, relationship.description, relationship.technology, relationship.interactionStlye, false)!; 18 | if(newRelationship){ 19 | newRelationship.linkedRelationshipId = relationship.id; 20 | } 21 | } 22 | } 23 | 24 | destination = destination.parent; 25 | } 26 | 27 | destination = relationship.destination; 28 | source = source.parent; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/core/model/createImpliedRelationshipsUnlessSameRelationshipExistsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Relationship, Element, AbstractImpliedRelationshipsStrategy } from '..'; 2 | 3 | export class CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy extends AbstractImpliedRelationshipsStrategy { 4 | 5 | createImpliedRelationships(relationship: Relationship): void { 6 | let source: Element | null = relationship.source; 7 | let destination: Element | null = relationship.destination; 8 | 9 | const model = source.model; 10 | 11 | while (source) { 12 | while (destination) { 13 | if (this.impliedRelationshipIsAllowed(source, destination)) { 14 | const createRelationship = !source.relationships.getEfferentRelationshipWith(destination, relationship.description); 15 | 16 | if (createRelationship) { 17 | const newRelationship = model.addRelationship(source, destination, relationship.description, relationship.technology, relationship.interactionStlye, false)!; 18 | 19 | if(newRelationship){ 20 | newRelationship.linkedRelationshipId = relationship.id; 21 | } 22 | } 23 | } 24 | 25 | destination = destination.parent; 26 | } 27 | 28 | destination = relationship.destination; 29 | source = source.parent; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/core/model/defaultImpliedRelationshipsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Relationship, AbstractImpliedRelationshipsStrategy } from '..'; 2 | 3 | export class DefaultImpliedRelationshipsStrategy extends AbstractImpliedRelationshipsStrategy { 4 | 5 | createImpliedRelationships(relationship: Relationship): void { 6 | // Do nothing 7 | } 8 | } -------------------------------------------------------------------------------- /src/core/model/deploymentElement.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "./element"; 2 | 3 | export abstract class DeploymentElement extends Element { 4 | static DefaultDeploymentEnvironment = "Default"; 5 | public environment: string = "Default"; 6 | } -------------------------------------------------------------------------------- /src/core/model/deploymentNode.ts: -------------------------------------------------------------------------------- 1 | import { DeploymentElement } from "./deploymentElement"; 2 | import { Element } from "./element"; 3 | import { ContainerInstance } from "./containerInstance"; 4 | import { Container } from "./container"; 5 | import { Relationship } from "./relationship"; 6 | 7 | export class DeploymentNode extends DeploymentElement { 8 | 9 | private _parent?: DeploymentNode | null; 10 | public get parent(): Element | null { 11 | return this._parent; 12 | } 13 | public set parent(p: Element | null) { 14 | this._parent = p && p.type == DeploymentNode.type ? p : null; 15 | } 16 | 17 | public static type = "DeploymentNode"; 18 | public get type(): string { return DeploymentNode.type; } 19 | 20 | public get canonicalName(): string { 21 | return this.parent 22 | ? this.parent.canonicalName + Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name) 23 | : Element.CanonicalNameSeparator + "Deployment" + Element.CanonicalNameSeparator + super.formatForCanonicalName(this.environment) + Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name); 24 | } 25 | 26 | public technology!: string; 27 | public instances!: number; 28 | public children: DeploymentNode[] = []; 29 | public containerInstances: ContainerInstance[] = []; 30 | 31 | public add(container: Container): ContainerInstance { 32 | var containerInstance = this.model.addContainerInstance(this, container); 33 | this.containerInstances.push(containerInstance); 34 | return containerInstance; 35 | } 36 | 37 | public addDeploymentNode(name: string, description: string, technology: string, instances = 1): DeploymentNode | null { 38 | var node = this.model.addDeploymentNode(name, description, technology, this, this.environment, instances); 39 | if (node) { 40 | this.children.push(node); 41 | } 42 | return node; 43 | } 44 | 45 | public uses(destination: DeploymentNode, description: string, technology: string): Relationship | null { 46 | return this.model.addRelationship(this, destination, description, technology); 47 | } 48 | 49 | public toDto(): any { 50 | var dto = super.toDto(); 51 | 52 | return { 53 | ...dto, 54 | environment: this.environment, 55 | technology: this.technology, 56 | instances: this.instances, 57 | children: this.children.map(h => h.toDto()), 58 | containerInstances: this.containerInstances.map(h => h.toDto()) 59 | }; 60 | } 61 | 62 | public fromDto(dto: any) { 63 | super.fromDto(dto); 64 | this.environment = dto.environment; 65 | this.technology = dto.technology; 66 | this.instances = dto.instances; 67 | this.children = dto.children ? dto.children.map((h: any) => { 68 | var c = new DeploymentNode(); 69 | c.fromDto(h); 70 | return c; 71 | }) : []; 72 | this.containerInstances = dto.containerInstances ? dto.containerInstances.map((h: any) => { 73 | var c = new ContainerInstance(); 74 | c.fromDto(h); 75 | return c; 76 | }) : []; 77 | } 78 | } -------------------------------------------------------------------------------- /src/core/model/element.ts: -------------------------------------------------------------------------------- 1 | import { ModelItem } from "./modelItem"; 2 | import { IEquatable } from "./iequatable"; 3 | import { Model } from "./model"; 4 | import { Relationships } from "./relationships"; 5 | 6 | 7 | export abstract class Element extends ModelItem implements IEquatable { 8 | 9 | public static CanonicalNameSeparator = "/"; 10 | 11 | public name!: string; 12 | public description!: string; 13 | public url!: string; 14 | public model!: Model; 15 | public relationships = new Relationships(this); 16 | 17 | public toDto(): any { 18 | var dto = super.toDto(); 19 | dto.name = this.name; 20 | dto.description = this.description; 21 | dto.url = this.url; 22 | dto.relationships = this.relationships.toDto(); 23 | return dto; 24 | } 25 | 26 | public fromDto(dto: any) { 27 | super.fromDto(dto); 28 | this.name = dto.name; 29 | this.description = dto.description; 30 | this.url = dto.url; 31 | this.relationships.fromDto(dto.relationships); 32 | } 33 | 34 | public abstract get canonicalName(): string; 35 | 36 | public abstract get parent(): Element | null; 37 | public abstract set parent(p: Element | null); 38 | 39 | public abstract get type(): string; 40 | 41 | public equals(other: Element): boolean { 42 | if (!other) { 43 | return false; 44 | } 45 | 46 | if (other === this) { 47 | return true; 48 | } 49 | 50 | if (other.type !== this.type) { 51 | return false; 52 | } 53 | 54 | return this.canonicalName === other.canonicalName; 55 | } 56 | 57 | protected formatForCanonicalName(name: string): string { 58 | return name.replace(Element.CanonicalNameSeparator, ""); 59 | } 60 | } -------------------------------------------------------------------------------- /src/core/model/httpHealthCheck.ts: -------------------------------------------------------------------------------- 1 | import { IEquatable } from "./iequatable"; 2 | 3 | export class HttpHealthCheck implements IEquatable{ 4 | 5 | public name!: string; 6 | public url!: string; 7 | public headers: { [key: string]: string } = {}; 8 | public interval!: number; 9 | public timeout!: number; 10 | 11 | equals(other: HttpHealthCheck): boolean { 12 | if (!other) { 13 | return false; 14 | } 15 | 16 | if (other === this) { 17 | return true; 18 | } 19 | return other.url === this.url; 20 | } 21 | 22 | public toDto(): any { 23 | return { 24 | name: this.name, 25 | url: this.url, 26 | interval: this.interval, 27 | timeout: this.timeout, 28 | headers: this.headers 29 | } 30 | } 31 | 32 | public fromDto(dto: any) { 33 | this.name = dto.name; 34 | this.url = dto.url; 35 | this.interval = dto.interval; 36 | this.timeout = dto.timeout; 37 | this.headers = dto.headers; 38 | } 39 | } -------------------------------------------------------------------------------- /src/core/model/iequatable.ts: -------------------------------------------------------------------------------- 1 | export interface IEquatable{ 2 | equals(other: T): boolean; 3 | } -------------------------------------------------------------------------------- /src/core/model/impliedRelationshipsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Relationship } from ".."; 2 | 3 | export interface IImpliedRelationshipsStrategy { 4 | createImpliedRelationships(relationship: Relationship): void; 5 | } -------------------------------------------------------------------------------- /src/core/model/interactionStyle.ts: -------------------------------------------------------------------------------- 1 | export enum InteractionStyle { 2 | Synchronous = "Synchronous", 3 | Asynchronous = "Asynchronous" 4 | } -------------------------------------------------------------------------------- /src/core/model/isChildOf.ts: -------------------------------------------------------------------------------- 1 | import { Element, Person } from ".."; 2 | 3 | export function isChildOf(e1: Element, e2: Element): boolean { 4 | if (e1.type === Person.type || e2.type === Person.type) { 5 | return false; 6 | } 7 | 8 | let parent = e2.parent; 9 | while (parent) { 10 | if (parent.id === e1.id) { 11 | return true; 12 | } 13 | 14 | parent = parent.parent; 15 | } 16 | 17 | return false; 18 | } -------------------------------------------------------------------------------- /src/core/model/location.ts: -------------------------------------------------------------------------------- 1 | export enum Location{ 2 | Unspecified = "Unspecified", 3 | Internal = "Internal", 4 | External = "External" 5 | } -------------------------------------------------------------------------------- /src/core/model/model.ts: -------------------------------------------------------------------------------- 1 | import { Relationship } from "./relationship"; 2 | import { Element } from "./element"; 3 | import { InteractionStyle } from "./interactionStyle"; 4 | import { SequentialIntegerIdGeneratorStrategy } from "./sequentialIntegerIdGeneratorStrategy"; 5 | import { Location } from "./location"; 6 | import { Person } from "./person"; 7 | import { SoftwareSystem } from "./softwareSystem"; 8 | import { Container } from "./container"; 9 | import { DeploymentNode } from "./deploymentNode"; 10 | import { ContainerInstance } from "./containerInstance"; 11 | import { Component } from "./component"; 12 | import { isChildOf } from "./isChildOf"; 13 | import { DefaultImpliedRelationshipsStrategy } from "./defaultImpliedRelationshipsStrategy"; 14 | import { IImpliedRelationshipsStrategy } from "./impliedRelationshipsStrategy"; 15 | 16 | export class Model { 17 | public relationships: Relationship[] = []; 18 | private _idGenerator = new SequentialIntegerIdGeneratorStrategy(); 19 | public people: Person[] = []; 20 | public softwareSystems: SoftwareSystem[] = []; 21 | public containerInstances: ContainerInstance[] = []; 22 | public deploymentNodes: DeploymentNode[] = []; 23 | public impliedRelationshipsStrategy: IImpliedRelationshipsStrategy = new DefaultImpliedRelationshipsStrategy(); 24 | private _elementsById: { [id: string]: Element } = {}; 25 | 26 | public toDto(): any { 27 | return { 28 | people: this.people.map(p => p.toDto()), 29 | softwareSystems: this.softwareSystems.map(s => s.toDto()), 30 | deploymentNodes: this.deploymentNodes.map(d => d.toDto()) 31 | }; 32 | } 33 | 34 | public fromDto(dto: any) { 35 | if (dto.people) { 36 | this.people = dto.people.map((personDto: any) => { 37 | var p = new Person(); 38 | p.fromDto(personDto); 39 | return p; 40 | }); 41 | } 42 | if (dto.softwareSystems) { 43 | this.softwareSystems = dto.softwareSystems.map((softwareSystemDto: any) => { 44 | var s = new SoftwareSystem(); 45 | s.fromDto(softwareSystemDto); 46 | return s; 47 | }); 48 | } 49 | if (dto.deploymentNodes) { 50 | this.deploymentNodes = dto.deploymentNodes.map((deploymentNodeDto: any) => { 51 | var d = new DeploymentNode(); 52 | d.fromDto(deploymentNodeDto); 53 | return d; 54 | }); 55 | } 56 | } 57 | 58 | public hydrate(): void { 59 | this.people.forEach(p => this.addElementToInternalStructures(p)); 60 | this.softwareSystems.forEach(s => { 61 | this.addElementToInternalStructures(s); 62 | s.containers.forEach(c => { 63 | this.addElementToInternalStructures(c); 64 | c.components.forEach(co => { 65 | this.addElementToInternalStructures(co); 66 | }); 67 | }); 68 | }); 69 | 70 | this.deploymentNodes.forEach(n => this.hydrateDeploymentNode(n, null)); 71 | 72 | this.people.forEach(p => this.hydrateRelationships(p)); 73 | this.softwareSystems.forEach(s => { 74 | this.hydrateRelationships(s); 75 | s.containers.forEach(c => { 76 | this.hydrateRelationships(c); 77 | c.components.forEach(co => this.hydrateRelationships(co)); 78 | }); 79 | }); 80 | 81 | this.deploymentNodes.forEach(n => this.hydrateDeploymentNodeRelationships(n)); 82 | } 83 | 84 | public hasRelationshipTargeting(target: Element): boolean { 85 | return this.relationships.some(r => r.destination.equals(target)); 86 | } 87 | 88 | public containsElement(element: Element): boolean { 89 | return this.getElement(element.id) == element; 90 | } 91 | 92 | public addRelationship(source: Element, destination: Element, description: string, technology?: string, interactionStyle = InteractionStyle.Synchronous, createImpliedRelationships = true): Relationship | null { 93 | var relationship = new Relationship(source, destination, description, technology, interactionStyle); 94 | 95 | if (isChildOf(source, destination) || isChildOf(destination, source)) { 96 | throw 'Relationships cannot be added between parents and children.'; 97 | } 98 | 99 | if (!source.relationships.has(relationship)) { 100 | relationship.id = this._idGenerator.generateId(relationship); 101 | source.relationships.add(relationship); 102 | this.addRelationshipToInternalStructures(relationship); 103 | 104 | if (createImpliedRelationships) { 105 | if 106 | ( 107 | (source.type === Person.type || source.type === SoftwareSystem.type || source.type === Container.type || source.type === Component.type) && 108 | (destination.type === Person.type || destination.type === SoftwareSystem.type || destination.type === Container.type || destination.type === Component.type) 109 | ) 110 | { 111 | this.impliedRelationshipsStrategy.createImpliedRelationships(relationship); 112 | } 113 | } 114 | 115 | return relationship; 116 | } 117 | 118 | return null; 119 | } 120 | 121 | public addPerson(name: string, description: string, location: Location = Location.Unspecified): Person | null { 122 | if (this.getPersonWithName(name)) { 123 | return null; 124 | } 125 | var person = new Person(); 126 | person.name = name; 127 | person.description = description; 128 | person.location = location; 129 | this.people.push(person); 130 | person.id = this._idGenerator.generateId(person); 131 | this.addElementToInternalStructures(person); 132 | return person; 133 | } 134 | 135 | public addSoftwareSystem(name: string, description: string, location: Location = Location.Unspecified): SoftwareSystem | null { 136 | if (this.getSoftwareSystemWithName(name)) { 137 | return null; 138 | } 139 | 140 | var softwareSystem = new SoftwareSystem(); 141 | softwareSystem.name = name; 142 | softwareSystem.description = description; 143 | softwareSystem.location = location; 144 | this.softwareSystems.push(softwareSystem); 145 | softwareSystem.id = this._idGenerator.generateId(softwareSystem); 146 | this.addElementToInternalStructures(softwareSystem); 147 | return softwareSystem; 148 | } 149 | 150 | public addContainer(parent: SoftwareSystem, name: string, description: string, technology: string): Container | null { 151 | if (parent.containers.some(c => c.name == name)) { 152 | return null; 153 | } 154 | 155 | var container = new Container(); 156 | container.name = name; 157 | container.description = description; 158 | container.technology = technology; 159 | container.parent = parent; 160 | parent.containers.push(container); 161 | container.id = this._idGenerator.generateId(container); 162 | this.addElementToInternalStructures(container); 163 | return container; 164 | } 165 | 166 | public addComponent(parent: Container, name: string, description: string, type?: string, technology?: string): Component | null { 167 | if (parent.components.some(c => c.name == name)) { 168 | return null; 169 | } 170 | 171 | var component = new Component(); 172 | component.name = name; 173 | component.description = description; 174 | component.technology = technology; 175 | component.parent = parent; 176 | 177 | if (type) { 178 | component.primaryType = type; 179 | } 180 | 181 | parent.components.push(component); 182 | component.id = this._idGenerator.generateId(component); 183 | this.addElementToInternalStructures(component); 184 | return component; 185 | } 186 | 187 | public addContainerInstance(deploymentNode: DeploymentNode, container: Container): ContainerInstance { 188 | var instanceNumber = this.containerInstances.filter(i => i.container!.equals(container)).length + 1; 189 | var instance = new ContainerInstance(); 190 | instance.container = container; 191 | instance.containerId = container.id; 192 | instance.instanceId = instanceNumber; 193 | instance.environment = deploymentNode.environment; 194 | instance.id = this._idGenerator.generateId(instance); 195 | 196 | var instancesInSameEnvironment = this.containerInstances.filter(f => f.environment === deploymentNode.environment); 197 | instancesInSameEnvironment.forEach(i => { 198 | 199 | var c = i.container!; 200 | container.relationships.forEach(r => { 201 | if (r.destination.equals(c)) { 202 | var newRelation = this.addRelationship(instance, i, r.description, r.technology, r.interactionStlye); 203 | if (newRelation) { 204 | newRelation.tags.clear(); 205 | newRelation.linkedRelationshipId = r.id; 206 | } 207 | } 208 | }); 209 | 210 | 211 | c.relationships.forEach(r => { 212 | if (r.destination.equals(container)) { 213 | var newRelation = this.addRelationship(i, instance, r.description, r.technology, r.interactionStlye); 214 | if (newRelation) { 215 | newRelation.tags.clear(); 216 | newRelation.linkedRelationshipId = r.id; 217 | } 218 | } 219 | }); 220 | }); 221 | 222 | this.addElementToInternalStructures(instance); 223 | this.containerInstances.push(instance); 224 | return instance; 225 | } 226 | 227 | public addDeploymentNode(name: string, description: string, technology: string, parent: DeploymentNode | null = null, environment = "Default", instances = 1): DeploymentNode | null { 228 | 229 | var nodes = parent ? parent.children : this.deploymentNodes; 230 | 231 | if (nodes.some(c => c.name == name && c.environment == environment)) { 232 | return null; 233 | } 234 | 235 | var node = new DeploymentNode(); 236 | node.name = name; 237 | node.description = description; 238 | node.technology = technology; 239 | node.environment = environment; 240 | node.instances = instances; 241 | node.parent = parent; 242 | 243 | if (!parent) { 244 | this.deploymentNodes.push(node); 245 | } 246 | node.id = this._idGenerator.generateId(node); 247 | this.addElementToInternalStructures(node); 248 | return node; 249 | } 250 | 251 | public getElement(id: string): Element { 252 | return this._elementsById[id]; 253 | } 254 | 255 | public getRelationship(id: string): Relationship | undefined { 256 | return this.relationships.find(r => r.id == id); 257 | } 258 | 259 | private addRelationshipToInternalStructures(relationship: Relationship) { 260 | this.relationships.push(relationship); 261 | this._idGenerator.found(relationship.id); 262 | } 263 | 264 | private addElementToInternalStructures(element: Element) { 265 | this._elementsById[element.id] = element; 266 | element.model = this; 267 | this._idGenerator.found(element.id); 268 | } 269 | 270 | private hydrateRelationships(element: Element) { 271 | element.relationships.forEach(r => { 272 | r.source = this.getElement(r.sourceId); 273 | r.destination = this.getElement(r.destinationId); 274 | this.addRelationshipToInternalStructures(r); 275 | }); 276 | } 277 | 278 | private hydrateDeploymentNode(deploymentNode: DeploymentNode, parent: DeploymentNode | null) { 279 | deploymentNode.parent = parent; 280 | this.addElementToInternalStructures(deploymentNode); 281 | 282 | deploymentNode.children.forEach(child => this.hydrateDeploymentNode(child, deploymentNode)); 283 | 284 | deploymentNode.containerInstances.forEach(containerInstance => { 285 | containerInstance.container = this._elementsById[containerInstance.containerId!]; 286 | this.addElementToInternalStructures(containerInstance); 287 | }); 288 | } 289 | 290 | private hydrateDeploymentNodeRelationships(deploymentNode: DeploymentNode) { 291 | this.hydrateRelationships(deploymentNode); 292 | deploymentNode.children.forEach(child => this.hydrateDeploymentNodeRelationships(child)); 293 | deploymentNode.containerInstances.forEach(c => this.hydrateRelationships(c)); 294 | } 295 | 296 | private getPersonWithName(name: string): Person | null { 297 | for (var i = 0; i < this.people.length; i++) { 298 | if (this.people[i].name == name) { 299 | return this.people[i]; 300 | } 301 | } 302 | return null; 303 | } 304 | 305 | private getSoftwareSystemWithName(name: string): SoftwareSystem | null { 306 | for (var i = 0; i < this.softwareSystems.length; i++) { 307 | if (this.softwareSystems[i].name == name) { 308 | return this.softwareSystems[i]; 309 | } 310 | } 311 | return null; 312 | } 313 | } -------------------------------------------------------------------------------- /src/core/model/modelItem.ts: -------------------------------------------------------------------------------- 1 | import { Tags } from "./tags"; 2 | 3 | export abstract class ModelItem { 4 | public id!: string; 5 | public properties!: Record; 6 | 7 | private _tags: Tags = new Tags(this); 8 | public get tags(): Tags { 9 | return this._tags; 10 | } 11 | 12 | public toDto(): any { 13 | return { 14 | id: this.id, 15 | tags: this.tags.toDto(), 16 | properties: this.properties 17 | } 18 | } 19 | 20 | public fromDto(dto: any) { 21 | this.id = dto.id; 22 | this.properties = dto.properties ?? {}; 23 | this.tags.fromDto(dto.tags); 24 | } 25 | 26 | public getRequiredTags(): string[] { 27 | return []; 28 | } 29 | } -------------------------------------------------------------------------------- /src/core/model/person.ts: -------------------------------------------------------------------------------- 1 | import { StaticStructureElement } from "./staticStructureElement"; 2 | import { IEquatable } from "./iequatable"; 3 | import { Location } from "./location"; 4 | import { Tags } from "./tags"; 5 | import { Element } from "./element"; 6 | import { InteractionStyle } from "./interactionStyle"; 7 | import { Relationship } from "./relationship"; 8 | 9 | export class Person extends StaticStructureElement implements IEquatable { 10 | 11 | public static type = "Person"; 12 | public get type(): string { return Person.type; } 13 | 14 | public location = Location.Unspecified; 15 | 16 | public get canonicalName(): string { 17 | return Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name); 18 | } 19 | 20 | public get parent(): Element | null { return null; } 21 | public set parent(p: Element | null) { } 22 | 23 | public getRequiredTags() { 24 | return [Tags.Element, Tags.Person]; 25 | } 26 | 27 | public interactsWith(destination: Person, description: string, technology?: string, interactionStyle: InteractionStyle = InteractionStyle.Synchronous): Relationship | null { 28 | return this.model.addRelationship(this, destination, description, technology, interactionStyle); 29 | } 30 | 31 | public delivers(destination: Person, description: string, technology?: string, interactionStyle? : InteractionStyle): Relationship | null { 32 | throw "Person cannot be the source of 'delivers' relations"; 33 | } 34 | 35 | public toDto() { 36 | var dto = super.toDto(); 37 | dto.location = this.location; 38 | return dto; 39 | } 40 | 41 | public fromDto(dto: any) { 42 | super.fromDto(dto); 43 | this.location = dto.location; 44 | } 45 | } -------------------------------------------------------------------------------- /src/core/model/relationship.ts: -------------------------------------------------------------------------------- 1 | import { ModelItem } from "./modelItem"; 2 | import { IEquatable } from "./iequatable"; 3 | import { Element } from "./element"; 4 | import { InteractionStyle } from "./interactionStyle"; 5 | import { Tags } from "./tags"; 6 | 7 | export class Relationship extends ModelItem implements IEquatable { 8 | 9 | public description!: string; 10 | public technology?: string; 11 | public interactionStlye = InteractionStyle.Synchronous; 12 | public sourceId!: string; 13 | public source!: Element; 14 | public destinationId!: string; 15 | public destination!: Element; 16 | public linkedRelationshipId?: string; 17 | 18 | constructor(source?: Element, destination?: Element, description?: string, technology?: string, interactionStyle: InteractionStyle = InteractionStyle.Synchronous) { 19 | super(); 20 | if (source) { 21 | this.source = source; 22 | this.sourceId = source.id; 23 | } 24 | 25 | if (destination) { 26 | this.destination = destination; 27 | this.destinationId = destination.id; 28 | } 29 | 30 | if (description) { 31 | this.description = description; 32 | } 33 | 34 | this.technology = technology; 35 | this.interactionStlye = interactionStyle; 36 | if (interactionStyle == InteractionStyle.Synchronous) { 37 | this.tags.add(Tags.Synchronous); 38 | } else { 39 | this.tags.add(Tags.Asynchronous); 40 | } 41 | } 42 | 43 | public toDto(): {} { 44 | var dto = super.toDto(); 45 | dto.description = this.description; 46 | if (this.technology) { 47 | dto.technology = this.technology; 48 | } 49 | if (this.interactionStlye != InteractionStyle.Synchronous) { 50 | dto.interactionStlye = this.interactionStlye; 51 | } 52 | dto.sourceId = this.sourceId; 53 | dto.destinationId = this.destinationId; 54 | if (this.linkedRelationshipId) { 55 | dto.linkedRelationshipId = this.linkedRelationshipId; 56 | } 57 | return dto; 58 | } 59 | 60 | public fromDto(dto: any) { 61 | super.fromDto(dto); 62 | this.description = dto.description; 63 | this.technology = dto.technology; 64 | this.interactionStlye = dto.interactionStlye; 65 | this.linkedRelationshipId = dto.linkedRelationshipId; 66 | this.sourceId = dto.sourceId; 67 | this.destinationId = dto.destinationId; 68 | } 69 | 70 | public getRequiredTags(): string[] { 71 | if (!this.linkedRelationshipId) { 72 | return [Tags.Relationship]; 73 | } 74 | return []; 75 | } 76 | 77 | equals(other: Relationship): boolean { 78 | if (!other) { 79 | return false; 80 | } 81 | 82 | if (this.description !== other.description) { 83 | return false; 84 | } 85 | 86 | if (!this.destination.equals(other.destination)) { 87 | return false; 88 | } 89 | 90 | if (!this.source.equals(other.source)) { 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /src/core/model/relationships.ts: -------------------------------------------------------------------------------- 1 | import { Relationship } from "./relationship"; 2 | import { Element } from "./element"; 3 | 4 | export class Relationships { 5 | 6 | private _all: Relationship[] = []; 7 | 8 | constructor(private owner: Element) { 9 | } 10 | 11 | public add(relationship: Relationship) { 12 | if (!this.has(relationship)) { 13 | this._all.push(relationship); 14 | } 15 | } 16 | 17 | public has(relationship: Relationship): boolean { 18 | return this._all.some(r => r.equals(relationship)); 19 | } 20 | 21 | public hasAfferentRelationships() { 22 | return this.owner.model.hasRelationshipTargeting(this.owner); 23 | } 24 | 25 | public getEfferentRelationshipWith(element: Element, description?: string): Relationship | null { 26 | if (!element) { 27 | return null; 28 | } 29 | 30 | let relationship = this._all.filter(r => r.destination.equals(element)); 31 | 32 | if (description) { 33 | relationship = relationship.filter(r => r.description === description); 34 | } 35 | 36 | if (relationship.length) { 37 | return relationship[0]; 38 | } 39 | 40 | return null; 41 | } 42 | 43 | public toDto(): {}[] { 44 | return this._all.map(r => r.toDto()); 45 | } 46 | 47 | public fromDto(dto: any[]) { 48 | this._all = dto 49 | ? dto.map((relationshipDto: any) => { 50 | var r = new Relationship(); 51 | r.fromDto(relationshipDto); 52 | return r; 53 | }) 54 | : []; 55 | } 56 | 57 | public forEach(callback: (r: Relationship) => void) { 58 | this._all.forEach(callback); 59 | } 60 | } -------------------------------------------------------------------------------- /src/core/model/role.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | ReadWrite = "ReadWrite", 3 | ReadOnly = "ReadOnly" 4 | } -------------------------------------------------------------------------------- /src/core/model/sequentialIntegerIdGeneratorStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "./element"; 2 | import { Relationship } from "./relationship"; 3 | 4 | export class SequentialIntegerIdGeneratorStrategy { 5 | private _id = 0; 6 | 7 | public found(id: string): void { 8 | var idInt = parseInt(id); 9 | if (idInt > this._id) { 10 | this._id = idInt; 11 | } 12 | } 13 | 14 | public generateId(item: Element | Relationship): string { 15 | this._id += 1; 16 | var idString = "" + this._id; 17 | return idString; 18 | } 19 | } -------------------------------------------------------------------------------- /src/core/model/softwareSystem.ts: -------------------------------------------------------------------------------- 1 | import { StaticStructureElement } from "./staticStructureElement"; 2 | import { IEquatable } from "./iequatable"; 3 | import { Element } from "./element"; 4 | import { Location } from "./location"; 5 | import { Tags } from "./tags"; 6 | import { Container } from "./container"; 7 | 8 | export class SoftwareSystem extends StaticStructureElement implements IEquatable{ 9 | 10 | public static type = "SoftwareSystem"; 11 | public get type(): string { return SoftwareSystem.type; } 12 | 13 | public get parent(): Element | null { return null; } 14 | public set parent(p: Element | null) { } 15 | 16 | public location = Location.Unspecified; 17 | 18 | public containers: Container[] = []; 19 | 20 | public get canonicalName(): string { 21 | return Element.CanonicalNameSeparator + super.formatForCanonicalName(this.name); 22 | } 23 | 24 | public toDto() { 25 | var dto = super.toDto(); 26 | dto.location = this.location; 27 | dto.containers = this.containers.map(c => c.toDto()); 28 | return dto; 29 | } 30 | 31 | public fromDto(dto: any) { 32 | super.fromDto(dto); 33 | this.location = dto.location; 34 | this.containers = dto.containers 35 | ? dto.containers.map((containerDto: any) => { 36 | var c = new Container(); 37 | c.parent = this; 38 | c.fromDto(containerDto); 39 | return c; 40 | }) 41 | : []; 42 | } 43 | 44 | public getRequiredTags() { 45 | return [Tags.Element, Tags.SoftwareSystem]; 46 | } 47 | 48 | public addContainer(name: string, description: string, technology: string): Container | null { 49 | return this.model.addContainer(this, name, description, technology); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/core/model/staticStructureElement.ts: -------------------------------------------------------------------------------- 1 | import { Element } from "./element"; 2 | import { SoftwareSystem } from "./softwareSystem"; 3 | import { Relationship } from "./relationship"; 4 | import { InteractionStyle } from "./interactionStyle"; 5 | import { Container } from "./container"; 6 | import {Component} from "./component"; 7 | import { Person } from "./person"; 8 | 9 | export abstract class StaticStructureElement extends Element { 10 | 11 | public uses(destination: SoftwareSystem | Container | Component, description: string, technology?: string, interactionStyle = InteractionStyle.Synchronous): Relationship | null { 12 | return this.model.addRelationship(this, destination, description, technology, interactionStyle); 13 | } 14 | 15 | public delivers(destination: Person, description: string, technology?: string, interactionStyle? : InteractionStyle): Relationship | null { 16 | return this.model.addRelationship(this, destination, description, technology, interactionStyle); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/model/tags.ts: -------------------------------------------------------------------------------- 1 | import { ModelItem } from "./modelItem"; 2 | 3 | export class Tags { 4 | 5 | public static Synchronous = "Synchronous"; 6 | public static Asynchronous = "Asynchronous"; 7 | public static Relationship = "Relationship"; 8 | public static Element = "Element"; 9 | public static Person = "Person"; 10 | public static SoftwareSystem = "Software System"; 11 | public static Container = "Container"; 12 | public static Component = "Component"; 13 | public static ContainerInstance = "Container Instance"; 14 | 15 | private _all: string[] = []; 16 | 17 | constructor(private _owner: ModelItem) { 18 | } 19 | 20 | public asArray(): string[] { 21 | return [...this._all]; 22 | } 23 | 24 | public add(tag: string): void { 25 | this._all.push(tag); 26 | } 27 | 28 | public remove(tag: string): void { 29 | this._all.splice(this._all.indexOf(tag), 1); 30 | } 31 | 32 | public contains(tag: string): boolean { 33 | return this._all.indexOf(tag) >= 0; 34 | } 35 | 36 | public clear() { 37 | this._all = []; 38 | } 39 | 40 | public toDto(): string { 41 | var all = [ 42 | ...this._all, 43 | ...this._owner.getRequiredTags() 44 | ]; 45 | if (all.length == 0) { 46 | return ""; 47 | } 48 | return all.join(","); 49 | } 50 | 51 | public fromDto(value: string) { 52 | this._all = value ? value.split(",") : []; 53 | } 54 | } -------------------------------------------------------------------------------- /src/core/model/user.ts: -------------------------------------------------------------------------------- 1 | import { IEquatable } from "./iequatable"; 2 | import { Role } from "./role"; 3 | 4 | export class User implements IEquatable { 5 | 6 | public username!: string; 7 | public role!: Role; 8 | 9 | equals(other: User): boolean { 10 | return other?.username === this.username && other?.role === this.role; 11 | } 12 | 13 | public toDto(): any { 14 | return { 15 | username: this.username, 16 | role: this.role 17 | }; 18 | } 19 | 20 | public fromDto(dto: any) { 21 | this.username = dto.username; 22 | this.role = dto.role; 23 | } 24 | } -------------------------------------------------------------------------------- /src/core/model/workspace.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "./model"; 2 | import { ViewSet } from "../view/viewSet"; 3 | import { Documentation } from "../documentation/documentation"; 4 | import { WorkspaceConfiguration } from "./workspaceConfiguration"; 5 | 6 | export abstract class AbstractWorkspace { 7 | public id!: number; 8 | public lastModifiedDate!: Date; 9 | public version!: string; 10 | public configuration = new WorkspaceConfiguration(); 11 | 12 | constructor(public name: string, public description: string) { 13 | } 14 | 15 | public toDto(): any { 16 | return { 17 | id: this.id, 18 | name: this.name, 19 | description: this.description, 20 | lastModifiedDate: this.lastModifiedDate, 21 | version: this.version, 22 | documentation: { 23 | sections: [], 24 | decisions: [], 25 | images: [] 26 | }, 27 | configuration: this.configuration.toDto() 28 | }; 29 | } 30 | 31 | public fromDto(dto: any) { 32 | this.id = dto.id; 33 | this.name = dto.name; 34 | this.description = dto.description; 35 | this.lastModifiedDate = dto.lastModifiedDate; 36 | this.version = dto.version; 37 | this.configuration.fromDto(dto.configuration) 38 | } 39 | } 40 | 41 | export class Workspace extends AbstractWorkspace { 42 | public model: Model = new Model(); 43 | public views: ViewSet = new ViewSet(this.model); 44 | public documentation: Documentation = new Documentation(this.model); 45 | 46 | public toDto(): any { 47 | const dto = super.toDto(); 48 | dto.model = this.model.toDto(); 49 | dto.views = this.views.toDto(); 50 | dto.documentation = this.documentation.toDto(); 51 | return dto; 52 | } 53 | 54 | public fromDto(dto: any) { 55 | super.fromDto(dto); 56 | this.model.fromDto(dto.model); 57 | this.views.fromDto(dto.views); 58 | this.documentation.fromDto(dto.documentation); 59 | } 60 | 61 | public hydrate(): void { 62 | this.model.hydrate(); 63 | this.views.hydrate(); 64 | this.documentation.hydrate(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/core/model/workspaceConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | import { Role } from "./role"; 3 | 4 | export enum WorkspaceScope { 5 | Landscape = "Landscape", 6 | SoftwareSystem = "SoftwareSystem" 7 | } 8 | 9 | export class WorkspaceConfiguration { 10 | 11 | private users: User[] = []; 12 | 13 | public scope : WorkspaceScope | null = null; 14 | 15 | public addUser(username: string, role: Role): void { 16 | const existingUser = this.users.find(u => u.username === username) 17 | if (existingUser) { 18 | if (existingUser.role !== role) { 19 | throw new Error("The user " + username + " already exists, but with a different role (" + existingUser.role + ")"); 20 | } 21 | return; 22 | } 23 | 24 | const user = new User(); 25 | user.username = username; 26 | user.role = role; 27 | this.users.push(user); 28 | } 29 | 30 | public toDto(): any { 31 | return { 32 | users: this.users.map(u => u.toDto()), 33 | scope: this.scope 34 | }; 35 | } 36 | 37 | public fromDto(dto: any) { 38 | if(!dto){ 39 | return; 40 | } 41 | this.scope = dto.scope; 42 | this.users = (dto.users as any[] ?? []).map(u => { 43 | const user = new User(); 44 | user.fromDto(u); 45 | return user; 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /src/core/view/automaticLayout.ts: -------------------------------------------------------------------------------- 1 | import { RankDirection } from "./rankDirection"; 2 | 3 | export class AutomaticLayout { 4 | public rankDirection?: RankDirection; 5 | public rankSeparation?: number; 6 | public nodeSeparation?: number; 7 | public edgeSeparation?: number; 8 | public vertices?: boolean; 9 | 10 | toDto() { 11 | return { 12 | rankDirection: this.rankDirection, 13 | rankSeparation: this.rankSeparation, 14 | nodeSeparation: this.nodeSeparation, 15 | edgeSeparation: this.edgeSeparation, 16 | vertices: this.vertices 17 | }; 18 | } 19 | 20 | fromDto(dto: any) { 21 | this.rankDirection = dto.rankDirection; 22 | this.rankSeparation = dto.rankSeparation; 23 | this.nodeSeparation = dto.nodeSeparation; 24 | this.edgeSeparation = dto.edgeSeparation; 25 | this.vertices = dto.vertices; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/view/branding.ts: -------------------------------------------------------------------------------- 1 | export interface Font { 2 | name: string; 3 | url?: string; 4 | } 5 | 6 | export interface Branding { 7 | font?: Font; 8 | logo?: string; 9 | } -------------------------------------------------------------------------------- /src/core/view/componentView.ts: -------------------------------------------------------------------------------- 1 | import { StaticView } from "./staticView"; 2 | import { SoftwareSystem } from "../model/softwareSystem"; 3 | import { Person } from "../model/person"; 4 | import { Container } from "../model/container"; 5 | import { Element } from "../model/element"; 6 | import { Component } from "../model/component"; 7 | 8 | export class ComponentView extends StaticView { 9 | 10 | public containerId?: string; 11 | 12 | public get name(): string { 13 | return this.softwareSystem!.name + " - " + this.container!.name + " - Components"; 14 | } 15 | 16 | constructor(public container?: Container, key?: string, description?: string) { 17 | super(container && container.softwareSystem ? container.softwareSystem : undefined, key, description); 18 | 19 | if(container){ 20 | this.containerId = container.id; 21 | } 22 | } 23 | 24 | public addAllElements(): void { 25 | this.addAllSoftwareSystems(); 26 | this.addAllPeople(); 27 | this.addAllContainers(); 28 | this.addAllComponents(); 29 | } 30 | 31 | public addAllComponents() { 32 | this.container?.components.forEach(c => this.addElement(c, true)); 33 | } 34 | 35 | public addNearestNeighbours(element: Element): void { 36 | this.addNearestNeighboursOfType(element, Person.type); 37 | this.addNearestNeighboursOfType(element, SoftwareSystem.type); 38 | this.addNearestNeighboursOfType(element, Container.type); 39 | this.addNearestNeighboursOfType(element, Component.type); 40 | } 41 | 42 | public addComponent(component: Component) { 43 | if (component.container != this.container) { 44 | return; 45 | } 46 | this.addElement(component, true); 47 | } 48 | 49 | protected addElement(element: Element, addRelationships: boolean): void { 50 | if (element === this.softwareSystem || element === this.container) { 51 | return; 52 | } 53 | 54 | super.addElement(element, addRelationships); 55 | } 56 | 57 | public toDto(): any { 58 | var dto = super.toDto(); 59 | dto.containerId = this.containerId; 60 | return dto; 61 | } 62 | 63 | public fromDto(dto: any): void { 64 | super.fromDto(dto); 65 | this.containerId = dto.containerId; 66 | } 67 | } -------------------------------------------------------------------------------- /src/core/view/containerView.ts: -------------------------------------------------------------------------------- 1 | import { StaticView } from "./staticView"; 2 | import { SoftwareSystem } from "../model/softwareSystem"; 3 | import { Person } from "../model/person"; 4 | import { Container } from "../model/container"; 5 | import { Element } from "../model/element"; 6 | 7 | export class ContainerView extends StaticView { 8 | 9 | public get name(): string { 10 | return this.softwareSystem!.name + " - Containers"; 11 | } 12 | 13 | constructor(softwareSystem?: SoftwareSystem, key?: string, description?: string) { 14 | super(softwareSystem, key, description); 15 | } 16 | 17 | public addAllElements(): void { 18 | this.addAllSoftwareSystems(); 19 | this.addAllPeople(); 20 | this.addAllContainers(); 21 | } 22 | 23 | public addNearestNeighbours(element: Element): void { 24 | this.addNearestNeighboursOfType(element, Person.type); 25 | this.addNearestNeighboursOfType(element, SoftwareSystem.type); 26 | this.addNearestNeighboursOfType(element, Container.type); 27 | } 28 | 29 | public addContainer(container: Container) { 30 | this.addElement(container, true); 31 | } 32 | 33 | public removeContainers(condition: (c: Container) => boolean): void { 34 | this.removeElements(e => e.type === Container.type && condition(e as Container)); 35 | } 36 | 37 | protected addElement(element: Element, addRelationships: boolean): void { 38 | if (element === this.softwareSystem) { 39 | return; 40 | } 41 | 42 | super.addElement(element, addRelationships); 43 | } 44 | } -------------------------------------------------------------------------------- /src/core/view/deploymentView.ts: -------------------------------------------------------------------------------- 1 | import { View } from "./view"; 2 | import { Model } from "../model/model"; 3 | import { DeploymentNode } from "../model/deploymentNode"; 4 | import { ContainerInstance } from "../model/containerInstance"; 5 | 6 | export class DeploymentView extends View { 7 | 8 | private _model!: Model; 9 | 10 | public get model(): Model { 11 | return this._model; 12 | } 13 | public set model(m: Model) { 14 | this._model = m; 15 | } 16 | 17 | public environment?: string; 18 | 19 | public toDto(): any { 20 | var dto = super.toDto(); 21 | dto.environment = this.environment; 22 | return dto; 23 | } 24 | 25 | public fromDto(dto: any) { 26 | super.fromDto(dto); 27 | this.environment = dto.environment; 28 | } 29 | 30 | public get name(): string { 31 | return this.softwareSystem ? this.softwareSystem.name + " - Deployment" : "Deployment"; 32 | } 33 | 34 | public addAllDeploymentNodes() { 35 | this.model.deploymentNodes.forEach(n => this.addDeploymentNode(n)); 36 | } 37 | 38 | public addDeploymentNode(deploymentNode: DeploymentNode) { 39 | if (deploymentNode && this.addContainerInstancesAndDeploymentNodes(deploymentNode)) { 40 | var parent = deploymentNode.parent; 41 | while (parent != null) { 42 | this.addElement(parent, false); 43 | parent = parent.parent; 44 | } 45 | } 46 | } 47 | 48 | public addContainerInstance(containerInstance: ContainerInstance) { 49 | this.addElement(containerInstance, true); 50 | let parent = containerInstance.parent as DeploymentNode; 51 | while (parent != null) { 52 | this.addElement(parent, true); 53 | parent = parent.parent as DeploymentNode; 54 | } 55 | } 56 | 57 | public removeDeploymentNode(deploymentNode: DeploymentNode) { 58 | deploymentNode.containerInstances.forEach(c => this.removeContainerInstance(c)); 59 | deploymentNode.children.forEach(c => this.removeDeploymentNode(c)); 60 | this.removeElement(deploymentNode); 61 | } 62 | 63 | public removeContainerInstance(containerInstance: ContainerInstance) { 64 | this.removeElement(containerInstance); 65 | } 66 | 67 | private addContainerInstancesAndDeploymentNodes(deploymentNode: DeploymentNode): boolean { 68 | var hasContainers = false; 69 | 70 | deploymentNode.containerInstances.forEach(containerInstance => { 71 | if (!this.softwareSystem || containerInstance.container!.parent!.equals(this.softwareSystem)) { 72 | this.addElement(containerInstance, true); 73 | hasContainers = true; 74 | } 75 | }); 76 | 77 | deploymentNode.children.forEach(child => { 78 | if (this.addContainerInstancesAndDeploymentNodes(child)) { 79 | hasContainers = true; 80 | } 81 | }); 82 | 83 | if (hasContainers) { 84 | this.addElement(deploymentNode, false); 85 | } 86 | 87 | return hasContainers; 88 | } 89 | } -------------------------------------------------------------------------------- /src/core/view/elementView.ts: -------------------------------------------------------------------------------- 1 | import { IEquatable } from "../model/iequatable"; 2 | import { Element } from "../model/element"; 3 | 4 | export class ElementView implements IEquatable{ 5 | public element!: Element; 6 | public id!: string; 7 | public x?: number; 8 | public y?: number; 9 | 10 | protected get type(): string { 11 | return "ElementView"; 12 | } 13 | 14 | constructor(element?: Element) { 15 | if (element) { 16 | this.element = element; 17 | this.id = element.id; 18 | } 19 | } 20 | 21 | public equals(other: ElementView): boolean { 22 | if (!other) { 23 | return false; 24 | } 25 | 26 | if (other === this) { 27 | return true; 28 | } 29 | 30 | if (other.type !== this.type) { 31 | return false; 32 | } 33 | 34 | return this.id === other.id; 35 | } 36 | 37 | public copyLayoutInformationFrom(source: ElementView) { 38 | if (source) { 39 | this.x = source.x; 40 | this.y = source.y; 41 | } 42 | } 43 | 44 | public toDto(): any { 45 | return { 46 | id: this.id, 47 | x: this.x, 48 | y: this.y 49 | } 50 | } 51 | 52 | 53 | public fromDto(dto: any): void { 54 | this.id = dto.id; 55 | this.x = dto.x; 56 | this.y = dto.y; 57 | } 58 | } -------------------------------------------------------------------------------- /src/core/view/filteredView.ts: -------------------------------------------------------------------------------- 1 | import { View } from "./view"; 2 | import { Tags } from "../model/tags"; 3 | 4 | export enum FilterMode { 5 | Include = "Include", 6 | Exclude = "Exclude" 7 | } 8 | 9 | export class FilteredView { 10 | public baseViewKey: string; 11 | public baseView?: View; 12 | public key: string; 13 | public order?: number; 14 | public description: string; 15 | public mode: FilterMode; 16 | public tags: string[]; 17 | 18 | constructor(baseView?: View, key?: string, description?: string, mode?: FilterMode, ...tags: string[]) { 19 | this.baseViewKey = baseView ? baseView.key : ""; 20 | this.baseView = baseView; 21 | this.key = key || ""; 22 | this.description = description || ""; 23 | this.mode = mode || FilterMode.Include; 24 | this.tags = tags; 25 | } 26 | 27 | public toDto(): any { 28 | return { 29 | key: this.key, 30 | order: this.order, 31 | description: this.description, 32 | mode: this.mode, 33 | tags: this.tags, 34 | baseViewKey: this.baseViewKey 35 | } 36 | } 37 | 38 | public fromDto(dto: any): void { 39 | this.key = dto.key; 40 | this.order = dto.order; 41 | this.description = dto.description; 42 | this.mode = dto.mode; 43 | this.tags = dto.tags; 44 | this.baseViewKey = dto.baseViewKey; 45 | } 46 | } -------------------------------------------------------------------------------- /src/core/view/paperSize.ts: -------------------------------------------------------------------------------- 1 | export enum Orientation { 2 | Portrait = "Portrait", 3 | Landscape = "Landscape" 4 | } 5 | 6 | export class PaperSize { 7 | 8 | private static paperSizes: { [key: string]: PaperSize } = {}; 9 | 10 | public static readonly A6_Portrait = new PaperSize("A6_Portrait", "A6", Orientation.Portrait, 1240, 1748); 11 | public static readonly A6_Landscape = new PaperSize("A6_Landscape", "A6", Orientation.Landscape, 1748, 1240); 12 | 13 | public static readonly A5_Portrait = new PaperSize("A5_Portrait", "A5", Orientation.Portrait, 1748, 2480); 14 | public static readonly A5_Landscape = new PaperSize("A5_Landscape", "A5", Orientation.Landscape, 2480, 1748); 15 | 16 | public static readonly A4_Portrait = new PaperSize("A4_Portrait", "A4", Orientation.Portrait, 2480, 3508); 17 | public static readonly A4_Landscape = new PaperSize("A4_Landscape", "A4", Orientation.Landscape, 3508, 2480); 18 | 19 | public static readonly A3_Portrait = new PaperSize("A3_Portrait", "A3", Orientation.Portrait, 3508, 4961); 20 | public static readonly A3_Landscape = new PaperSize("A3_Landscape", "A3", Orientation.Landscape, 4961, 3508); 21 | 22 | public static readonly A2_Portrait = new PaperSize("A2_Portrait", "A2", Orientation.Portrait, 4961, 7016); 23 | public static readonly A2_Landscape = new PaperSize("A2_Landscape", "A2", Orientation.Landscape, 7016, 4961); 24 | 25 | public static readonly A1_Portrait = new PaperSize("A1_Portrait", "A1", Orientation.Portrait, 7016, 9933); 26 | public static readonly A1_Landscape = new PaperSize("A1_Landscape", "A1", Orientation.Landscape, 9933, 7016); 27 | 28 | public static readonly A0_Portrait = new PaperSize("A0_Portrait", "A0", Orientation.Portrait, 9933, 14043); 29 | public static readonly A0_Landscape = new PaperSize("A0_Landscape", "A0", Orientation.Landscape, 14043, 9933); 30 | 31 | public static readonly Letter_Portrait = new PaperSize("Letter_Portrait", "Letter", Orientation.Portrait, 2550, 3300); 32 | public static readonly Letter_Landscape = new PaperSize("Letter_Landscape", "Letter", Orientation.Landscape, 3300, 2550); 33 | 34 | public static readonly Legal_Portrait = new PaperSize("Legal_Portrait", "Legal", Orientation.Portrait, 2550, 4200); 35 | public static readonly Legal_Landscape = new PaperSize("Legal_Landscape", "Legal", Orientation.Landscape, 4200, 2550); 36 | 37 | public static readonly Slide_4_3 = new PaperSize("Slide_4_3", "Slide 4:3", Orientation.Landscape, 3306, 2480); 38 | public static readonly Slide_16_9 = new PaperSize("Slide_16_9", "Slide 16:9", Orientation.Landscape, 3508, 1973); 39 | 40 | private constructor(public key: string, public name: string, public orientation: Orientation, public width: number, public height: number) { 41 | PaperSize.paperSizes[key] = this; 42 | } 43 | 44 | public static getPaperSize(key:string){ 45 | return this.paperSizes[key] || this.A4_Portrait; 46 | } 47 | } -------------------------------------------------------------------------------- /src/core/view/rankDirection.ts: -------------------------------------------------------------------------------- 1 | export enum RankDirection { 2 | TopBottom = "TopBottom", 3 | BottomTop = "BottomTop", 4 | LeftRight = "LeftRight", 5 | RightLeft = "RightLeft" 6 | } 7 | -------------------------------------------------------------------------------- /src/core/view/relationshipView.ts: -------------------------------------------------------------------------------- 1 | import { IEquatable } from "../model/iequatable"; 2 | import { Relationship } from "../model/relationship"; 3 | 4 | export enum Routing { 5 | Direct = "Direct", 6 | Orthogonal = "Orthogonal" 7 | } 8 | 9 | export class RelationshipView implements IEquatable{ 10 | public relationship!: Relationship; 11 | public id!: string; 12 | public order?: string; 13 | public description!: string; 14 | public vertices: { x?: number, y?: number }[] = []; 15 | public routing?: Routing; 16 | 17 | protected get type(): string { 18 | return "RelationshipView"; 19 | } 20 | 21 | private _position?: number = undefined; 22 | public get position(): number | undefined { 23 | return this._position; 24 | } 25 | public set position(value: number | undefined) { 26 | if (value != undefined) { 27 | if (value < 0) { 28 | this._position = 0; 29 | } 30 | else if (value > 100) { 31 | this._position = 100; 32 | } 33 | else { 34 | this._position = value; 35 | } 36 | } 37 | } 38 | 39 | constructor(relationship?: Relationship) { 40 | if (relationship) { 41 | this.relationship = relationship; 42 | this.id = relationship.id; 43 | } 44 | } 45 | 46 | public equals(other: RelationshipView): boolean { 47 | if (!other) { 48 | return false; 49 | } 50 | 51 | if (other === this) { 52 | return true; 53 | } 54 | 55 | if (other.type !== this.type) { 56 | return false; 57 | } 58 | 59 | if (this.description != other.description) { 60 | return false; 61 | } 62 | 63 | if (this.id != other.id) { 64 | return false; 65 | } 66 | 67 | return !(this.order != undefined ? this.order != other.order : other.order != undefined) 68 | } 69 | 70 | public copyLayoutInformationFrom(source: RelationshipView) { 71 | if (source) { 72 | this.vertices = source.vertices; 73 | this.routing = source.routing; 74 | this.position = source.position; 75 | } 76 | } 77 | 78 | public toDto(): any { 79 | return { 80 | id: this.id, 81 | order: this.order, 82 | description: this.description, 83 | vertices: this.vertices, 84 | routing: this.routing, 85 | position: this.position 86 | } 87 | } 88 | 89 | 90 | public fromDto(dto: any): void { 91 | this.id = dto.id; 92 | this.order = dto.order; 93 | this.description = dto.description; 94 | this.vertices = dto.vertices; 95 | this.routing = dto.routing 96 | this.position = dto.position; 97 | } 98 | } -------------------------------------------------------------------------------- /src/core/view/staticView.ts: -------------------------------------------------------------------------------- 1 | import { View } from "./view"; 2 | import { SoftwareSystem } from "../model/softwareSystem"; 3 | import { Person } from "../model/person"; 4 | import { Element } from "../model/element"; 5 | 6 | export abstract class StaticView extends View { 7 | 8 | public toDto(): any { 9 | var dto = super.toDto(); 10 | dto.animations = []; 11 | return dto; 12 | } 13 | 14 | public abstract addAllElements(): void; 15 | public abstract addNearestNeighbours(element: Element): void; 16 | 17 | public addAllSoftwareSystems(): void { 18 | this.model.softwareSystems.forEach(s => { 19 | this.addSoftwareSystem(s); 20 | }); 21 | } 22 | 23 | public addAllPeople(): void { 24 | this.model.people.forEach(p => { 25 | this.addPerson(p); 26 | }) 27 | } 28 | 29 | public addAllContainers(): void { 30 | this.softwareSystem!.containers.forEach(c => this.addElement(c, true)); 31 | } 32 | 33 | public addSoftwareSystem(softwareSystem: SoftwareSystem) { 34 | this.addElement(softwareSystem, true); 35 | } 36 | 37 | public addPerson(person: Person) { 38 | this.addElement(person, true); 39 | } 40 | 41 | public addNearestNeighboursOfType(element: Element, typeOfElement: string): void { 42 | if (!element) { 43 | return; 44 | } 45 | 46 | this.addElement(element, true); 47 | 48 | this.model.relationships.forEach(r => { 49 | if (r.source.equals(element) && r.destination.type == typeOfElement) { 50 | this.addElement(r.destination, true); 51 | } 52 | 53 | if (r.destination.equals(element) && r.source.type == typeOfElement) { 54 | this.addElement(r.source, true); 55 | } 56 | }); 57 | } 58 | } -------------------------------------------------------------------------------- /src/core/view/styles.ts: -------------------------------------------------------------------------------- 1 | import { Routing } from "./relationshipView"; 2 | 3 | export enum Shape { 4 | Box = "Box", 5 | RoundedBox = "RoundedBox", 6 | Circle = "Circle", 7 | Ellipse = "Ellipse", 8 | Hexagon = "Hexagon", 9 | Cylinder = "Cylinder", 10 | Pipe = "Pipe", 11 | Person = "Person", 12 | Robot = "Robot", 13 | Folder = "Folder", 14 | WebBrowser = "WebBrowser", 15 | MobileDevicePortrait = "MobileDevicePortrait", 16 | MobileDeviceLandscape = "MobileDeviceLandscape" 17 | } 18 | 19 | export enum Border { 20 | Solid = "Solid", 21 | Dashed = "Dashed" 22 | } 23 | 24 | export interface IRelationshipStyle { 25 | thickness?: number; 26 | color?: string; 27 | fontSize?: number; 28 | width?: number; 29 | dashed?: boolean; 30 | routing?: Routing; 31 | opacity?: number; 32 | position?: number; 33 | } 34 | 35 | export class RelationshipStyle implements IRelationshipStyle { 36 | 37 | public thickness?: number; 38 | public color?: string; 39 | public fontSize?: number; 40 | public width?: number; 41 | public dashed?: boolean; 42 | public routing?: Routing; 43 | public opacity?: number; 44 | public position?: number; 45 | 46 | constructor(public tag: string) { } 47 | 48 | public toDto(): any { 49 | return JSON.parse(JSON.stringify(this)); 50 | } 51 | 52 | public fromDto(dto: any): RelationshipStyle { 53 | const self: any = this; 54 | for (let field in dto) { 55 | self[field] = dto[field]; 56 | } 57 | return this; 58 | } 59 | } 60 | 61 | export interface IElementStyle { 62 | width?: number; 63 | height?: number; 64 | background?: string; 65 | color?: string; 66 | stroke?: string; 67 | fontSize?: number; 68 | shape?: Shape; 69 | icon?: string; 70 | border?: Border; 71 | opacity?: number; 72 | metadata?: boolean; 73 | description?: boolean; 74 | } 75 | 76 | export class ElementStyle implements IElementStyle { 77 | public width?: number; 78 | public height?: number; 79 | public background?: string; 80 | public color?: string; 81 | public stroke?: string; 82 | public fontSize?: number; 83 | public shape?: Shape; 84 | public icon?: string; 85 | public border?: Border; 86 | public opacity?: number; 87 | public metadata?: boolean; 88 | public description?: boolean; 89 | 90 | constructor(public tag: string) { } 91 | 92 | public toDto(): any { 93 | return JSON.parse(JSON.stringify(this)); 94 | } 95 | 96 | public fromDto(dto: any): ElementStyle { 97 | const self: any = this; 98 | for (let field in dto) { 99 | self[field] = dto[field]; 100 | } 101 | return this; 102 | } 103 | } 104 | 105 | export interface ITheme { 106 | relationships: IRelationshipStyle[]; 107 | elements: IElementStyle[]; 108 | } 109 | 110 | export class Styles { 111 | private relationships: RelationshipStyle[] = []; 112 | private elements: ElementStyle[] = []; 113 | 114 | public addRelationshipStyle(style: RelationshipStyle) { 115 | this.relationships.push(style); 116 | } 117 | 118 | public addElementStyle(style: ElementStyle) { 119 | this.elements.push(style); 120 | } 121 | 122 | public toDto(): any { 123 | return { 124 | relationships: this.relationships.map(r => r.toDto()), 125 | elements: this.elements.map(r => r.toDto()), 126 | } 127 | } 128 | 129 | public fromDto(dto: any): void { 130 | this.relationships = (dto.relationships || []).map((r: any) => new RelationshipStyle(r.tag).fromDto(r)) 131 | this.elements = (dto.elements || []).map((e: any) => new ElementStyle(e.tag).fromDto(e)) 132 | } 133 | 134 | public toTheme(): ITheme { 135 | return { 136 | elements: this.elements, 137 | relationships: this.relationships 138 | }; 139 | } 140 | } -------------------------------------------------------------------------------- /src/core/view/systemContextView.ts: -------------------------------------------------------------------------------- 1 | import { StaticView } from "./staticView"; 2 | import { SoftwareSystem } from "../model/softwareSystem"; 3 | import { Element } from "../model/element"; 4 | import { Person } from "../model/person"; 5 | 6 | export class SystemContextView extends StaticView { 7 | 8 | public get name(): string { 9 | return this.softwareSystem!.name + " - System Context"; 10 | } 11 | 12 | constructor(softwareSystem?: SoftwareSystem, key?: string, description?: string) { 13 | super(softwareSystem, key, description); 14 | if (softwareSystem) { 15 | this.addElement(softwareSystem, true); 16 | } 17 | } 18 | 19 | public addAllElements(): void { 20 | this.addAllSoftwareSystems(); 21 | this.addAllPeople(); 22 | } 23 | 24 | public addNearestNeighbours(element: Element): void { 25 | this.addNearestNeighboursOfType(element, SoftwareSystem.type); 26 | this.addNearestNeighboursOfType(element, Person.type); 27 | } 28 | } -------------------------------------------------------------------------------- /src/core/view/terminology.ts: -------------------------------------------------------------------------------- 1 | export interface Terminology { 2 | enterprise?: string; 3 | person?: string; 4 | softwareSystem?: string; 5 | container?: string; 6 | component?: string; 7 | code?: string; 8 | deploymentNode?: string; 9 | relationship?: string; 10 | } -------------------------------------------------------------------------------- /src/core/view/view.ts: -------------------------------------------------------------------------------- 1 | import { SoftwareSystem } from "../model/softwareSystem"; 2 | import { Model } from "../model/model"; 3 | import { ElementView } from "./elementView"; 4 | import { RelationshipView } from "./relationshipView"; 5 | import { Element } from "../model/element"; 6 | import { Relationship } from "../model/relationship"; 7 | import { AutomaticLayout } from "./automaticLayout"; 8 | import { RankDirection } from "./rankDirection"; 9 | import { PaperSize } from "./paperSize"; 10 | 11 | export abstract class View { 12 | public key!: string; 13 | public description!: string; 14 | public title!: string; 15 | public softwareSystemId?: string; 16 | public softwareSystem?: SoftwareSystem; 17 | public elements: ElementView[] = []; 18 | public relationships: RelationshipView[] = []; 19 | public automaticLayout?: AutomaticLayout; 20 | public paperSize?: PaperSize; 21 | public order?: number; 22 | 23 | public get model(): Model { 24 | return this.softwareSystem!.model; 25 | } 26 | public set model(m: Model) { 27 | } 28 | 29 | public abstract get name(): string; 30 | 31 | constructor(softwareSystem?: SoftwareSystem, key?: string, description?: string) { 32 | if (softwareSystem) { 33 | this.softwareSystem = softwareSystem; 34 | this.softwareSystemId = softwareSystem ? softwareSystem.id : undefined; 35 | this.key = key!; 36 | this.description = description!; 37 | } 38 | } 39 | 40 | public toDto(): any { 41 | return { 42 | key: this.key, 43 | order: this.order, 44 | description: this.description, 45 | softwareSystemId: this.softwareSystemId, 46 | title: this.title, 47 | elements: this.elements.map(e => e.toDto()), 48 | relationships: this.relationships.map(r => r.toDto()), 49 | automaticLayout: this.automaticLayout ? this.automaticLayout.toDto() : null, 50 | paperSize: this.paperSize?.key 51 | } 52 | } 53 | 54 | public fromDto(dto: any): void { 55 | this.key = dto.key; 56 | this.order = dto.order; 57 | this.description = dto.description; 58 | this.softwareSystemId = dto.softwareSystemId; 59 | this.title = dto.title; 60 | this.elements = (dto.elements || []).map((elementDto: any) => { 61 | var e = new ElementView(); 62 | e.fromDto(elementDto); 63 | return e; 64 | }); 65 | this.relationships = (dto.relationships || []).map((relationshipDto: any) => { 66 | var r = new RelationshipView(); 67 | r.fromDto(relationshipDto); 68 | return r; 69 | }); 70 | if (dto.automaticLayout) { 71 | this.automaticLayout = new AutomaticLayout(); 72 | this.automaticLayout.fromDto(dto.automaticLayout); 73 | } 74 | if (dto.paperSize) { 75 | this.paperSize = PaperSize.getPaperSize(dto.paperSize); 76 | } 77 | } 78 | 79 | public add(relationship: Relationship): RelationshipView | null { 80 | if (relationship && this.isElementInView(relationship.source) && this.isElementInView(relationship.destination)) { 81 | return this._addRelationship(relationship); 82 | } 83 | return null; 84 | } 85 | 86 | public addRelationship(relationship: Relationship, description: string, order: string): RelationshipView | null { 87 | var view = this.add(relationship); 88 | if (view) { 89 | view.description = description; 90 | view.order = order; 91 | } 92 | return view; 93 | } 94 | 95 | public isElementInView(element: Element): boolean { 96 | return this.elements.some(e => e.element.equals(element)); 97 | } 98 | 99 | public remove(relationship: Relationship) { 100 | if (relationship) { 101 | this.relationships = this.relationships.filter(r => !r.relationship.equals(relationship)); 102 | } 103 | } 104 | 105 | public copyLayoutInformationFrom(source: View) { 106 | if (!this.paperSize) { 107 | this.paperSize = source.paperSize; 108 | } 109 | 110 | source.elements.forEach(e => { 111 | var target = this.elements.find(t => t.element.equals(e.element)); 112 | if (target) { 113 | target.copyLayoutInformationFrom(e); 114 | } 115 | }); 116 | 117 | source.relationships.forEach(r => { 118 | var target = this.relationships.find(t => t.relationship.equals(r.relationship)); 119 | if (target) { 120 | target.copyLayoutInformationFrom(r); 121 | } 122 | }); 123 | } 124 | 125 | public setAutomaticLayout(enable: boolean): void; 126 | public setAutomaticLayout(direction: RankDirection, rankSeparation: number, nodeSeparation: number, edgeSeparation: number, vertices: boolean): void; 127 | public setAutomaticLayout(directionOrEnable: RankDirection | boolean, rankSeparation?: number, nodeSeparation?: number, edgeSeparation?: number, vertices?: boolean): void { 128 | if (typeof directionOrEnable === 'boolean') { 129 | if (directionOrEnable) { 130 | this.automaticLayout = new AutomaticLayout(); 131 | this.automaticLayout.fromDto({ rankDirection: RankDirection.TopBottom, rankSeparation: 300, nodeSeparation: 600, edgeSeparation: 200, vertices: false }); 132 | } else { 133 | this.automaticLayout = undefined; 134 | } 135 | } else { 136 | this.automaticLayout = new AutomaticLayout(); 137 | this.automaticLayout.fromDto({ rankDirection: directionOrEnable, rankSeparation, nodeSeparation, edgeSeparation, vertices }); 138 | } 139 | } 140 | 141 | public removeElements(condition: (e: Element) => boolean): void { 142 | const remove = this.elements.filter(e => condition(e.element)); 143 | remove.forEach(e => this.removeElement(e.element)); 144 | } 145 | 146 | protected addElement(element: Element, addRelationships: boolean): void { 147 | if (element) { 148 | 149 | if (this.elements.some(e => e.element === element)) { 150 | return; 151 | } 152 | 153 | if (this.model.containsElement(element)) { 154 | this.elements.push(new ElementView(element)); 155 | if (addRelationships) { 156 | this.addRelationships(element); 157 | } 158 | } 159 | } 160 | } 161 | 162 | protected removeElement(element: Element): void { 163 | if (element) { 164 | this.elements = this.elements.filter(e => !e.element.equals(element)); 165 | this.relationships = this.relationships.filter(r => !r.relationship.source.equals(element) && !r.relationship.destination.equals(element)) 166 | } 167 | } 168 | 169 | private addRelationships(element: Element): void { 170 | var elements = this.elements.map(e => e.element); 171 | element.relationships.forEach(r => { 172 | if (elements.some(e => e.equals(r.destination))) { 173 | this._addRelationship(r); 174 | } 175 | }); 176 | elements.forEach(e => { 177 | e.relationships.forEach(r => { 178 | if (r.destination.equals(element)) { 179 | this._addRelationship(r); 180 | } 181 | }); 182 | }); 183 | } 184 | 185 | private _addRelationship(relationship: Relationship): RelationshipView { 186 | var view = this.relationships.find(r => r.relationship.equals(relationship)); 187 | if (!view) { 188 | view = new RelationshipView(relationship); 189 | this.relationships.push(view); 190 | } 191 | 192 | return view; 193 | } 194 | } -------------------------------------------------------------------------------- /src/core/view/viewConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { Styles } from "./styles"; 2 | import { Terminology } from "./terminology"; 3 | import { Branding } from "./branding"; 4 | 5 | export class ViewConfiguration { 6 | public styles: Styles = new Styles(); 7 | 8 | public theme?: string; 9 | 10 | public terminology: Terminology = {}; 11 | public branding: Branding = {}; 12 | 13 | public toDto(): any { 14 | return { 15 | styles: this.styles.toDto(), 16 | branding: this.branding, 17 | terminology: this.terminology, 18 | viewSortOrder: "Default", 19 | theme: this.theme 20 | }; 21 | } 22 | 23 | public fromDto(dto: any): void { 24 | if (dto.styles) { 25 | this.styles.fromDto(dto.styles); 26 | } 27 | if (dto.terminology) { 28 | this.terminology = dto.terminology; 29 | } 30 | if (dto.branding) { 31 | this.branding = dto.branding; 32 | } 33 | this.theme = dto.theme; 34 | } 35 | } -------------------------------------------------------------------------------- /src/core/view/viewSet.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "../model/model"; 2 | import { SystemContextView } from "./systemContextView"; 3 | import { SoftwareSystem } from "../model/softwareSystem"; 4 | import { View } from "./view"; 5 | import { ContainerView } from "./containerView"; 6 | import { DeploymentView } from "./deploymentView"; 7 | import { ViewConfiguration } from "./viewConfiguration"; 8 | import { ComponentView } from "./componentView"; 9 | import { Container } from "../model/container"; 10 | import { FilteredView, FilterMode } from "./filteredView"; 11 | import { StaticView } from "./staticView"; 12 | 13 | export class ViewSet { 14 | 15 | public systemContextViews: SystemContextView[] = []; 16 | public containerViews: ContainerView[] = []; 17 | public componentViews: ComponentView[] = []; 18 | public deploymentViews: DeploymentView[] = []; 19 | public filteredViews: FilteredView[] = []; 20 | public configuration = new ViewConfiguration(); 21 | 22 | constructor(public model: Model) { 23 | } 24 | 25 | public createSystemContextView(softwareSystem: SoftwareSystem, key: string, description: string): SystemContextView { 26 | this.assertThatTheViewKeyIsUnique(key); 27 | var view = new SystemContextView(softwareSystem, key, description); 28 | view.order = this.getNextOrder(); 29 | this.systemContextViews.push(view); 30 | return view; 31 | } 32 | 33 | public createContainerView(softwareSystem: SoftwareSystem, key: string, description: string): ContainerView { 34 | this.assertThatTheViewKeyIsUnique(key); 35 | var view = new ContainerView(softwareSystem, key, description); 36 | view.order = this.getNextOrder(); 37 | this.containerViews.push(view); 38 | return view; 39 | } 40 | 41 | public createComponentView(container: Container, key: string, description: string): ComponentView { 42 | this.assertThatTheViewKeyIsUnique(key); 43 | var view = new ComponentView(container, key, description); 44 | view.order = this.getNextOrder(); 45 | this.componentViews.push(view); 46 | return view; 47 | } 48 | 49 | public createDeploymentView(key: string, description: string, softwareSystem?: SoftwareSystem): DeploymentView { 50 | this.assertThatTheViewKeyIsUnique(key); 51 | var view = new DeploymentView(softwareSystem, key, description); 52 | view.order = this.getNextOrder(); 53 | view.model = this.model; 54 | this.deploymentViews.push(view); 55 | return view; 56 | } 57 | 58 | public createFilteredView(view: StaticView, key: string, description: string, mode: FilterMode, ...tags: string[]): FilteredView { 59 | this.assertThatTheViewKeyIsUnique(key); 60 | 61 | const filteredView = new FilteredView(view, key, description, mode, ...tags); 62 | filteredView.order = this.getNextOrder(); 63 | this.filteredViews.push(filteredView); 64 | 65 | return filteredView; 66 | } 67 | 68 | public toDto(): any { 69 | return { 70 | systemLandscapeViews: [], 71 | systemContextViews: this.systemContextViews.map(v => v.toDto()), 72 | containerViews: this.containerViews.map(v => v.toDto()), 73 | componentViews: this.componentViews.map(v => v.toDto()), 74 | dynamicViews: [], 75 | deploymentViews: this.deploymentViews.map(v => v.toDto()), 76 | filteredViews: this.filteredViews.map(v => v.toDto()), 77 | configuration: this.configuration.toDto() 78 | }; 79 | } 80 | 81 | public fromDto(dto: any): void { 82 | this.systemContextViews = this.viewsFromDto(dto.systemContextViews, () => new SystemContextView()); 83 | this.containerViews = this.viewsFromDto(dto.containerViews, () => new ContainerView()); 84 | this.componentViews = this.viewsFromDto(dto.componentViews, () => new ComponentView()); 85 | this.deploymentViews = this.viewsFromDto(dto.deploymentViews, () => new DeploymentView()); 86 | this.filteredViews = this.viewsFromDto(dto.filteredViews, () => new FilteredView()) 87 | if (dto.configuration) { 88 | this.configuration.fromDto(dto.configuration); 89 | } 90 | } 91 | 92 | public hydrate(): void { 93 | this.systemContextViews.forEach(v => { 94 | v.softwareSystem = this.model.softwareSystems.find(s => s.id === v.softwareSystemId)!; 95 | this.hydrateView(v); 96 | }); 97 | 98 | this.containerViews.forEach(v => { 99 | v.softwareSystem = this.model.softwareSystems.find(s => s.id === v.softwareSystemId)!; 100 | this.hydrateView(v); 101 | }); 102 | 103 | this.componentViews.forEach(v => { 104 | v.container = this.model.getElement(v.containerId!); 105 | v.softwareSystem = v.container.softwareSystem!; 106 | this.hydrateView(v); 107 | }); 108 | 109 | this.deploymentViews.forEach(v => { 110 | if (v.softwareSystemId) { 111 | v.softwareSystem = this.model.softwareSystems.find(s => s.id == v.softwareSystemId)!; 112 | } 113 | v.model = this.model; 114 | this.hydrateView(v); 115 | }); 116 | 117 | this.filteredViews.forEach(v => { 118 | v.baseView = this.getViewWithKey(v.baseViewKey); 119 | }); 120 | } 121 | 122 | public copyLayoutInformationFrom(source: ViewSet) { 123 | this.systemContextViews.forEach(v => { 124 | var s = ViewSet.findView(v.key, source.systemContextViews); 125 | if (s) { 126 | v.copyLayoutInformationFrom(s); 127 | } 128 | }); 129 | this.containerViews.forEach(v => { 130 | var s = ViewSet.findView(v.key, source.containerViews); 131 | if (s) { 132 | v.copyLayoutInformationFrom(s); 133 | } 134 | }); 135 | this.deploymentViews.forEach(v => { 136 | var s = ViewSet.findView(v.key, source.deploymentViews); 137 | if (s) { 138 | v.copyLayoutInformationFrom(s); 139 | } 140 | }); 141 | } 142 | 143 | public getViewWithKey(key: string): View | undefined { 144 | if (!key) { 145 | throw "A key must be specified."; 146 | } 147 | 148 | return this.systemContextViews.find(v => v.key == key) 149 | || this.containerViews.find(v => v.key == key) 150 | || this.componentViews.find(v => v.key == key) 151 | || this.deploymentViews.find(v => v.key == key); 152 | } 153 | 154 | private assertThatTheViewKeyIsUnique(key: string): void { 155 | if (this.getViewWithKey(key) || this.filteredViews.some(v => v.key == key)) { 156 | throw "A view with the key " + key + " already exists."; 157 | } 158 | } 159 | 160 | private static findView(key: string, views: TView[]): TView | undefined { 161 | return views.find(v => v.key == key); 162 | } 163 | 164 | private hydrateView(view: View): void { 165 | view.elements.forEach(e => { 166 | e.element = this.model.getElement(e.id); 167 | }); 168 | view.relationships.forEach(r => { 169 | r.relationship = this.model.getRelationship(r.id)!; 170 | }); 171 | } 172 | 173 | private viewsFromDto(viewDtos: any[], ctor: () => TView): TView[] { 174 | if (!viewDtos) { 175 | return []; 176 | } 177 | 178 | return viewDtos.map((viewDto: any) => { 179 | var view = ctor(); 180 | view.fromDto(viewDto); 181 | return view; 182 | }); 183 | } 184 | 185 | private getNextOrder(): number { 186 | let maxOrder = 0; 187 | [ 188 | ...this.systemContextViews, 189 | ...this.containerViews, 190 | ...this.componentViews, 191 | ...this.deploymentViews, 192 | ...this.filteredViews 193 | ].forEach(v => { 194 | if (v.order && v.order > maxOrder) { 195 | maxOrder = v.order; 196 | } 197 | }) 198 | 199 | return maxOrder + 1; 200 | } 201 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './client'; -------------------------------------------------------------------------------- /test/api-compatibility.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { StructurizrClient } from "../src"; 3 | import { createWorkspace } from "./workspace"; 4 | 5 | export async function testApiCompatitbility() { 6 | const apiKey = process.env.STRUCTURIZR_API_KEY! 7 | const apiSecret = process.env.STRUCTURIZR_API_SECRET! 8 | const workspaceId = process.env.STRUCTURIZR_WORKSPACE_ID!; 9 | 10 | var client = new StructurizrClient(apiKey, apiSecret); 11 | 12 | await wait(1000); 13 | const resultJson = await client.putWorkspace(parseInt(workspaceId, 10), createWorkspace()); 14 | const result = tryParse(resultJson); 15 | expect(result.success).to.be.true; 16 | expect(result.message).to.equal("OK"); 17 | } 18 | 19 | export async function testApiIdempotency() { 20 | await testApiCompatitbility(); 21 | const apiKey = process.env.STRUCTURIZR_API_KEY! 22 | const apiSecret = process.env.STRUCTURIZR_API_SECRET! 23 | const workspaceId = process.env.STRUCTURIZR_WORKSPACE_ID!; 24 | var client = new StructurizrClient(apiKey, apiSecret); 25 | 26 | await wait(1000); 27 | const expectedWorkspace = await client.getWorkspace(parseInt(workspaceId, 10)); 28 | 29 | await testApiCompatitbility(); 30 | 31 | await wait(1000); 32 | const actualWorkspace = await client.getWorkspace(parseInt(workspaceId, 10)); 33 | expect(actualWorkspace.lastModifiedDate).not.to.be.null; 34 | actualWorkspace.lastModifiedDate = expectedWorkspace.lastModifiedDate; 35 | expect(actualWorkspace.toDto()).to.deep.equalInAnyOrder(expectedWorkspace.toDto()); 36 | } 37 | 38 | function tryParse(json: string): any { 39 | try { 40 | return JSON.parse(json); 41 | } 42 | catch (e: any) { 43 | throw new Error(`Could not parse JSON, error ${e}, json:${json}`); 44 | } 45 | } 46 | 47 | function wait(delay: number): Promise { 48 | return new Promise(r => setTimeout(r, delay)); 49 | } -------------------------------------------------------------------------------- /test/delivers.ts: -------------------------------------------------------------------------------- 1 | import { it } from "mocha"; 2 | import { expect } from "chai"; 3 | import { Workspace } from "../src"; 4 | 5 | export const delivers = () => { 6 | it("should add relationship", () => { 7 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 8 | 9 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 10 | const factoryWorker = workspace.model.addPerson("Monkey Factory floor worker", "Works in the production line")!; 11 | const relation = factory.delivers(factoryWorker, "Production problem alerts"); 12 | 13 | expect(relation).not.to.be.null; 14 | expect(relation?.description).to.equal("Production problem alerts"); 15 | }); 16 | 17 | it("should fail for Person", () => { 18 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 19 | 20 | const factoryWorker = workspace.model.addPerson("Monkey Factory floor worker", "Works in the production line")!; 21 | const systemAdmin = workspace.model.addPerson("Monkey Factory admin", "Administrates the solution")!; 22 | 23 | expect(() => factoryWorker.delivers(systemAdmin, "Support requests")).to.throw("Person cannot be the source of 'delivers' relations"); 24 | }); 25 | } -------------------------------------------------------------------------------- /test/implicitRelationships.ts: -------------------------------------------------------------------------------- 1 | import { it, describe } from "mocha"; 2 | import { expect } from "chai"; 3 | import { Workspace, CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy, CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy } from "../src"; 4 | 5 | export const implicitRelationships = () => { 6 | 7 | 8 | describe("DefaultImpliedRelationshipsStrategy", () => { 9 | 10 | it("should not add any implicit relationships", () => { 11 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 12 | 13 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 14 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 15 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 16 | ingress.uses(crm, "Get customer data", "HTTP"); 17 | 18 | const implicitRelation = factory.relationships.getEfferentRelationshipWith(crm); 19 | expect(implicitRelation).to.be.null; 20 | }); 21 | 22 | }); 23 | 24 | describe("CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy", () => { 25 | 26 | it("should add implicit relation if no relationship exists ", () => { 27 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 28 | workspace.model.impliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy(); 29 | 30 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 31 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 32 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 33 | ingress.uses(crm, "Get customer data", "HTTP"); 34 | 35 | const implicitRelation = factory.relationships.getEfferentRelationshipWith(crm); 36 | expect(implicitRelation).not.to.be.null; 37 | expect(implicitRelation?.description).to.equal("Get customer data"); 38 | expect(implicitRelation?.technology).to.equal("HTTP"); 39 | }); 40 | 41 | it("should not add implicit relation if any relationship exists ", () => { 42 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 43 | workspace.model.impliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy(); 44 | 45 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 46 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 47 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 48 | factory.uses(crm, "Load customer data") 49 | ingress.uses(crm, "Get customer data", "HTTP"); 50 | 51 | const explicitRelation = factory.relationships.getEfferentRelationshipWith(crm, "Load customer data"); 52 | expect(explicitRelation).not.to.be.null; 53 | expect(explicitRelation?.description).to.equal("Load customer data"); 54 | expect(explicitRelation?.technology).to.be.undefined; 55 | 56 | const implicitRelation = factory.relationships.getEfferentRelationshipWith(crm, "Get customer data"); 57 | expect(implicitRelation).to.be.null; 58 | }); 59 | 60 | }); 61 | 62 | describe("CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy", () => { 63 | 64 | it("should add implicit relation if no relationship exists ", () => { 65 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 66 | workspace.model.impliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy(); 67 | 68 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 69 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 70 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 71 | ingress.uses(crm, "Get customer data", "HTTP"); 72 | 73 | const implicitRelation = factory.relationships.getEfferentRelationshipWith(crm); 74 | expect(implicitRelation).not.to.be.null; 75 | expect(implicitRelation?.description).to.equal("Get customer data"); 76 | expect(implicitRelation?.technology).to.equal("HTTP"); 77 | }); 78 | 79 | it("should add implicit relation if other relationship exists ", () => { 80 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 81 | workspace.model.impliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy(); 82 | 83 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 84 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 85 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 86 | factory.uses(crm, "Load customer data") 87 | ingress.uses(crm, "Get customer data", "HTTP"); 88 | 89 | const explicitRelation = factory.relationships.getEfferentRelationshipWith(crm, "Load customer data"); 90 | expect(explicitRelation).not.to.be.null; 91 | expect(explicitRelation?.description).to.equal("Load customer data"); 92 | expect(explicitRelation?.technology).to.be.undefined; 93 | 94 | const implicitRelation = factory.relationships.getEfferentRelationshipWith(crm, "Get customer data"); 95 | expect(implicitRelation).not.to.be.null; 96 | expect(implicitRelation?.description).to.equal("Get customer data"); 97 | expect(implicitRelation?.technology).to.equal("HTTP"); 98 | }); 99 | 100 | it("should not add implicit relation if same relationship exists ", () => { 101 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 102 | workspace.model.impliedRelationshipsStrategy = new CreateImpliedRelationshipsUnlessSameRelationshipExistsStrategy(); 103 | 104 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 105 | const crm = workspace.model.addSoftwareSystem("Monkey CRM", "Stores and manages customer data")!; 106 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 107 | factory.uses(crm, "Get customer data") 108 | ingress.uses(crm, "Get customer data", "HTTP"); 109 | 110 | const explicitRelation = factory.relationships.getEfferentRelationshipWith(crm, "Get customer data"); 111 | expect(explicitRelation).not.to.be.null; 112 | expect(explicitRelation?.description).to.equal("Get customer data"); 113 | expect(explicitRelation?.technology).to.be.undefined; 114 | }); 115 | 116 | }); 117 | } -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "mocha"; 2 | import { use } from 'chai'; 3 | import { testApiCompatitbility, testApiIdempotency } from "./api-compatibility"; 4 | import { testPlantUMLWriter, testPlantUMLWriterIsAbleToHandleProperlyPackageNameWithMultipleWords } from "./plantUMLWriter"; 5 | import { testElementStyleThemeExport, testFullThemeExport } from "./themeExport"; 6 | import { default as deepEqualInAnyOrder } from 'deep-equal-in-any-order'; 7 | import { regression } from "./regression"; 8 | import { implicitRelationships } from "./implicitRelationships"; 9 | import { delivers } from "./delivers"; 10 | use(deepEqualInAnyOrder); 11 | 12 | 13 | describe("structurizr-typescript", () => { 14 | 15 | describe("api", () => { 16 | it("should be compatible to the server side API", testApiCompatitbility).timeout(15000); 17 | it("merging unchanged workspaces should be idempotent", testApiIdempotency).timeout(30000); 18 | }); 19 | 20 | describe("client", () => { 21 | describe("plantUML", () => { 22 | it("export plant UML diagrams correctly", testPlantUMLWriter); 23 | it( 24 | "handles long system names properly when exporting as package in container view", 25 | testPlantUMLWriterIsAbleToHandleProperlyPackageNameWithMultipleWords 26 | ); 27 | }) 28 | }); 29 | 30 | describe("core", () => { 31 | describe("theme export", () => { 32 | it("should not export undefined fields", testElementStyleThemeExport); 33 | it("should export element and relationship styles", testFullThemeExport); 34 | }); 35 | }); 36 | 37 | describe("regression", regression); 38 | 39 | describe("implicitRelationships", implicitRelationships); 40 | 41 | describe("delivers", delivers); 42 | }); -------------------------------------------------------------------------------- /test/plantUMLWriter.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { PlantUMLWriter, Workspace } from "../src"; 3 | import { createWorkspace, createWorkspaceWithSoftwareSystemNameOfMoreThanTwoWords } from "./workspace"; 4 | 5 | export function testPlantUMLWriter() { 6 | const plantUML = new PlantUMLWriter().toPlantUML(createWorkspace()); 7 | expect(plantUML).not.to.be.empty; 8 | } 9 | 10 | export function testPlantUMLWriterIsAbleToHandleProperlyPackageNameWithMultipleWords() { 11 | const workspace: Workspace = createWorkspaceWithSoftwareSystemNameOfMoreThanTwoWords(); 12 | 13 | const expected = "@startuml\r\ntitle GPS tracking system - Containers\r\ncaption Container view for the GPS tracking system\r\npackage \"GPS tracking system\" {\r\n}\r\n@enduml"; 14 | const result = new PlantUMLWriter().toPlantUML(workspace); 15 | 16 | expect(result.trim()).to.eq(expected); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /test/regression.ts: -------------------------------------------------------------------------------- 1 | import { it } from "mocha"; 2 | import { expect } from "chai"; 3 | import { Workspace } from "../src"; 4 | 5 | export const regression = () => { 6 | it("should not return null when adding relations", () => { 7 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 8 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 9 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 10 | const storage = factory.addContainer("storage", "stores telemetry data", "Table Storage")!; 11 | 12 | const relation = ingress.uses(storage, "Store telemetry", "IoT Hub Routing"); 13 | 14 | expect(relation).not.to.be.null; 15 | }); 16 | } -------------------------------------------------------------------------------- /test/themeExport.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Styles, ElementStyle, Shape, RelationshipStyle, Routing, ITheme } from "../src"; 3 | 4 | export function testElementStyleThemeExport() { 5 | const styles = createElementStyles(); 6 | const expectedTheme = createElementTheme(false); 7 | 8 | var actualTheme = JSON.parse(JSON.stringify(styles.toTheme())); 9 | 10 | expect(actualTheme).to.deep.equalInAnyOrder(expectedTheme); 11 | } 12 | 13 | export function testFullThemeExport() { 14 | const styles = createElementStyles(); 15 | 16 | const asyncStyle = new RelationshipStyle("async"); 17 | asyncStyle.dashed = true; 18 | asyncStyle.routing = Routing.Orthogonal; 19 | styles.addRelationshipStyle(asyncStyle) 20 | 21 | const expectedTheme = createElementTheme(true); 22 | 23 | var actualTheme = JSON.parse(JSON.stringify(styles.toTheme())); 24 | 25 | expect(actualTheme).to.deep.equalInAnyOrder(expectedTheme); 26 | } 27 | 28 | function createElementStyles() { 29 | const styles = new Styles(); 30 | 31 | const systemStyle = new ElementStyle("Software System"); 32 | systemStyle.background = "#1168bd"; 33 | systemStyle.color = "#ffffff"; 34 | 35 | const personStyle = new ElementStyle("Person"); 36 | personStyle.background = "#08427b"; 37 | personStyle.color = "#ffffff"; 38 | personStyle.shape = Shape.Person; 39 | 40 | styles.addElementStyle(systemStyle); 41 | styles.addElementStyle(personStyle); 42 | 43 | return styles; 44 | } 45 | 46 | function createElementTheme(addRelationshipStyle: boolean) { 47 | return { 48 | "elements": [ 49 | { 50 | "tag": "Software System", 51 | "background": "#1168bd", 52 | "color": "#ffffff" 53 | }, 54 | { 55 | "tag": "Person", 56 | "background": "#08427b", 57 | "color": "#ffffff", 58 | "shape": "Person" 59 | } 60 | ], 61 | "relationships": addRelationshipStyle ? [{ 62 | "tag": "async", 63 | "dashed": true, 64 | "routing": "Orthogonal" 65 | }]: [] 66 | }; 67 | } -------------------------------------------------------------------------------- /test/workspace.ts: -------------------------------------------------------------------------------- 1 | import { Workspace, Location, InteractionStyle, ElementStyle, RelationshipStyle, Shape, Tags, Format, DecisionStatus, RankDirection, FilterMode, PaperSize, SoftwareSystem, Role, WorkspaceScope } from "../src"; 2 | 3 | export const createWorkspace: () => Workspace = () => { 4 | const workspace = new Workspace("Monkey Factory - Test", "Description"); 5 | 6 | const user = workspace.model.addPerson("User", "uses the system")!; 7 | 8 | const admin = workspace.model.addPerson("Admin", "administers the system and manages user")!; 9 | 10 | admin!.interactsWith(user!, "manages rights"); 11 | 12 | const factory = workspace.model.addSoftwareSystem("Monkey Factory", "Oversees the production of stuffed monkey animals")!; 13 | factory.location = Location.Internal; 14 | 15 | const ingress = factory.addContainer("ingress", "accepts incoming telemetry data", "IoT Hub")!; 16 | ingress.tags.add("queue"); 17 | 18 | const storage = factory.addContainer("storage", "stores telemetry data", "Table Storage")!; 19 | storage.tags.add("database"); 20 | 21 | const frontend = factory.addContainer("frontend", "visualizes telemetry data", "React")!; 22 | ingress.uses(storage, "store telemetry", "IoT Hub routing", InteractionStyle.Asynchronous); 23 | frontend.uses(storage, "load telemetry data", "Table Storage SDK"); 24 | 25 | frontend.addComponent("user account", "allows the user to signup or sign in, and see his profile"); 26 | const dashboard = frontend.addComponent("dashboard", "allows the user get an overvew of telementry data", "src/components/dashboard", "Typescript")!; 27 | dashboard.primaryCodeElement!.language = "TypeScript"; 28 | const chart = dashboard.addSupportingType("src/components/chart")!; 29 | chart.language = "TypeScript"; 30 | 31 | const crm = workspace.model.addSoftwareSystem("CRM system", "manage tickets")!; 32 | crm.location = Location.External; 33 | factory.uses(crm, "Create tickets", "AMQP", InteractionStyle.Asynchronous); 34 | 35 | user.uses(dashboard, "view dashboards"); 36 | user.uses(factory, "view dashboards"); 37 | user.uses(frontend, "view dashboards"); 38 | admin.uses(factory, "configure users"); 39 | admin.uses(frontend, "configure users"); 40 | admin.uses(crm, "work on tickets"); 41 | 42 | const ingressNode = workspace.model.addDeploymentNode("IoT Hub", "Ingress", "Azure IoT Hub", null, "DEV", 2)!; 43 | ingressNode.add(ingress); 44 | 45 | const storageNode = workspace.model.addDeploymentNode("Storage", "Storage", "Azure Storage Account with web hosting enabled", null, "DEV", 1)!; 46 | storageNode.add(storage); 47 | storageNode.add(frontend);; 48 | 49 | const systemContext = workspace.views.createSystemContextView(factory, "factory-context", "The system context view for the monkey factory"); 50 | systemContext.addNearestNeighbours(factory); 51 | systemContext.setAutomaticLayout(true); 52 | 53 | const containerView = workspace.views.createContainerView(factory, "factory-containers", "Container view for the monkey factory"); 54 | containerView.addAllContainers(); 55 | containerView.addNearestNeighbours(factory); 56 | containerView.setAutomaticLayout(RankDirection.LeftRight, 100, 200, 100, true); 57 | 58 | const frontendComponentView = workspace.views.createComponentView(frontend, "factory-frontend-components", "Component View for the monkey factory frontend"); 59 | frontendComponentView.addAllComponents(); 60 | frontendComponentView.addNearestNeighbours(frontend); 61 | frontendComponentView.paperSize = PaperSize.A3_Portrait; 62 | 63 | const containerViewForFiltering = workspace.views.createContainerView(factory, "factory-containers-filtering", "Container view for the monkey factory"); 64 | containerView.addAllContainers(); 65 | containerView.addNearestNeighbours(factory); 66 | containerView.setAutomaticLayout(RankDirection.LeftRight, 100, 200, 100, true); 67 | 68 | workspace.views.createFilteredView(containerViewForFiltering, "databases", "Shows all databases", FilterMode.Include, "database"); 69 | workspace.views.createFilteredView(containerViewForFiltering, "people", "Shows all people", FilterMode.Include, Tags.Person); 70 | 71 | const deploymentView = workspace.views.createDeploymentView("factory-deployment", "The deployment view fo the monkey factory", factory); 72 | deploymentView.addAllDeploymentNodes(); 73 | 74 | const dbStyle = new ElementStyle("database"); 75 | dbStyle.shape = Shape.Cylinder; 76 | 77 | const queueStyle = new ElementStyle("queue"); 78 | queueStyle.shape = Shape.Pipe; 79 | 80 | const syncStyle = new RelationshipStyle(Tags.Synchronous); 81 | syncStyle.dashed = false; 82 | 83 | const asyncStyle = new RelationshipStyle(Tags.Asynchronous); 84 | asyncStyle.dashed = true; 85 | 86 | workspace.views.configuration.styles.addElementStyle(dbStyle); 87 | workspace.views.configuration.styles.addElementStyle(queueStyle); 88 | workspace.views.configuration.styles.addRelationshipStyle(asyncStyle); 89 | workspace.views.configuration.styles.addRelationshipStyle(syncStyle); 90 | workspace.views.configuration.theme = "https://static.structurizr.com/themes/microsoft-azure-2023.01.24/theme.json"; 91 | 92 | workspace.documentation.addSection(factory, "Monkey Factory", Format.Markdown, `The monkey factory oversees the production of stuffed monkey animals`); 93 | workspace.documentation.addSection(frontend, "Frontend", Format.AsciiDoc, `The frontend is written in javascript`); 94 | workspace.documentation.addSection(undefined, "Unrelated", Format.AsciiDoc, `Text goes here`); 95 | workspace.documentation.addDecision(factory, '1', new Date('2008-09-15T15:53:00'), 'Use ISO 8601 Format for Dates', DecisionStatus.Accepted, Format.Markdown, `We should use ISO 8601`); 96 | workspace.documentation.addDecision(undefined, '2', new Date('2008-09-15T15:53:00'), 'Use angular as the frontend framework', DecisionStatus.Proposed, Format.Markdown, `We should use angular`); 97 | 98 | workspace.views.configuration.terminology.person = "Actor"; 99 | 100 | workspace.configuration.addUser("nouser@nodomain", Role.ReadOnly); 101 | 102 | workspace.configuration.scope = WorkspaceScope.SoftwareSystem; 103 | 104 | return workspace; 105 | } 106 | 107 | export const createWorkspaceWithSoftwareSystemNameOfMoreThanTwoWords: () => Workspace = () => { 108 | const workspace: Workspace = new Workspace("GPS tracking system", "Description"); 109 | 110 | const gpsTrackingSystem: SoftwareSystem = workspace.model.addSoftwareSystem("GPS tracking system", "Ingests, processes and visualizes GPS tracking data") as SoftwareSystem; 111 | gpsTrackingSystem.location = Location.Internal; 112 | 113 | const containerView = workspace.views.createContainerView(gpsTrackingSystem, "gps-tracking-system-containers", "Container view for the GPS tracking system"); 114 | containerView.addAllContainers(); 115 | containerView.addNearestNeighbours(gpsTrackingSystem); 116 | 117 | return workspace; 118 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "exclude": ["dist", "test", "sample"] 12 | } 13 | --------------------------------------------------------------------------------