├── .gitignore
├── LICENSE
├── README.md
├── dist
├── examples
│ ├── basic-api.json
│ ├── firehose.json
│ ├── full-app.json
│ ├── iot.json
│ ├── s3-processing.json
│ └── stream-test.json
├── img
│ ├── aws
│ │ ├── AWS-Lambda_Function.png
│ │ ├── AWS-Step-Functions.png
│ │ ├── Amazon-API-Gateway.png
│ │ ├── Amazon-CloudWatch_Event-Time-Based.png
│ │ ├── Amazon-Cognito.png
│ │ ├── Amazon-DynamoDB_Table.png
│ │ ├── Amazon-Kinesis-Data-Analytics.png
│ │ ├── Amazon-Kinesis-Data-Firehose.png
│ │ ├── Amazon-Kinesis-Data-Streams.png
│ │ ├── Amazon-S3_Bucket.png
│ │ ├── Amazon-SNS_Topic.png
│ │ └── IoT_Rule.png
│ └── logo.png
└── index.html
├── package-lock.json
├── package.json
├── src
├── engines
│ ├── sam.js
│ └── servfrmwk.js
└── index.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | node_modules/
4 | dist/main.*
5 | dist/img/vis/*
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Danilo Poccia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Serverless By Design
2 |
3 | 
4 |
5 | Serverless By Design is a visual approach to serverless development:
6 |
7 | - An application is a network of _nodes_ (serverless resources, such as Lambda functions or S3 buckets) connected by _edges_ (their relationships, for example a trigger or a data flow)
8 | - _Edit_ an application adding nodes and edges following an _event-driven_ design
9 | - _Import_ a previously exported application to continue working on it
10 | - Choose a _runtime_, and _build_ your application (for example, using AWS SAM)
11 | - Optionally use _canary_ or _linear_ deployments for your future updates
12 | - Edit _templates_ and code files for the final configurations before deploying the application
13 | - _Export_ an application to save it for later use in a JSON file
14 | - Take a _picture_ of the application architecture to have a visual representation to share
15 | - Fine tune the _physics_ used to place nodes and edges on the screen, for example enable/disable it or choose another solver
16 |
17 | Serverless By Design runs in the browser and doesn't need an internet connection when installed locally.
18 |
19 | A live version is available at: http://sbd.danilop.net
20 |
21 | Think. Build. Repeat.
22 |
23 | ## License
24 |
25 | Copyright (c) 2017 Danilo Poccia, http://danilop.net
26 |
27 | This code is licensed under the The MIT License (MIT). Please see the LICENSE file that accompanies this project for the terms of use.
28 |
29 |
30 | ## Installation
31 |
32 | You need `node` and `npm`. Just run:
33 |
34 | ```
35 | npm run build
36 | ```
37 |
38 | to build it for production, then open `dist/index.html` with your favourite browser.
39 |
40 | For a development build, that you can debug with a browser, use:
41 |
42 | ```
43 | npm run dev
44 | ```
45 |
46 |
47 | ## Usage
48 |
49 | Here are a few examples to help you start:
50 |
51 | - [Basic API](https://sbd.danilop.net/?import=examples/basic-api.json)
52 | - [S3 Processing](https://sbd.danilop.net/?import=examples/s3-processing.json)
53 | - [Firehose Processing API](https://sbd.danilop.net/?import=examples/firehose.json)
54 | - [Streaming Analytics](https://sbd.danilop.net/?import=examples/stream-test.json)
55 | - [Some IoT](https://sbd.danilop.net/?import=examples/iot.json)
56 | - [All Together Now](https://sbd.danilop.net/?import=examples/full-app.json)
57 |
58 |
59 | ## Dependencies
60 |
61 | This code depends on:
62 | - [Vis.js](http://visjs.org)
63 | - [js-yaml](http://nodeca.github.io/js-yaml/)
64 | - [FileSaver.js](https://github.com/eligrey/FileSaver.js/)
65 | - [jszip](https://stuk.github.io/jszip/)
66 | - [font-awesome](http://fontawesome.io)
67 | - [JQuery](https://jquery.com)
68 |
--------------------------------------------------------------------------------
/dist/examples/basic-api.json:
--------------------------------------------------------------------------------
1 | { "nodes": [{ "id": "MyApi", "x": -472, "y": -172.5, "label": "API Gateway\nMyApi", "title": "My REST API", "model": { "type": "api", "description": "My REST API" }, "group": "api", "shadow": false }, { "id": "MyBackEnd", "x": -271, "y": -158.5, "label": "Lambda Function\nMyBackEnd", "title": "My REST Back End", "model": { "type": "fn", "description": "My REST Back End" }, "group": "fn", "shadow": false }, { "id": "MyTable", "x": -85, "y": -149.5, "label": "DynamoDB Table\nMyTable", "title": "My Database Table", "model": { "type": "table", "description": "My Database Table" }, "group": "table", "shadow": false }], "edges": [{ "from": "MyApi", "to": "MyBackEnd", "label": "integration", "color": { "color": "blue" }, "id": "96ce4c49-5a81-4a25-9b21-2e7745076036" }, { "from": "MyBackEnd", "to": "MyTable", "label": "read/write", "color": { "color": "green" }, "dashes": true, "id": "a6d31a48-50f6-421b-9411-a8bf582d1887" }] }
2 |
--------------------------------------------------------------------------------
/dist/examples/firehose.json:
--------------------------------------------------------------------------------
1 | {"nodes":[{"id":"MyFirehose","x":-273.8228840125392,"y":-66.99921630094045,"label":"Kinesis Firehose\nMyFirehose","model":{"type":"deliveryStream","description":""},"group":"deliveryStream","shadow":false},{"id":"MyBucket","x":-85.44827586206897,"y":-78.00391849529781,"label":"S3 Bucket\nMyBucket","model":{"type":"bucket","description":""},"group":"bucket","shadow":false},{"id":"TransformFirehose","x":-203.26332288401255,"y":68.94122257053291,"label":"Lambda Function\nTransformFirehose","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"ProcessEvents","x":-308.7789968652038,"y":-24.27507836990596,"label":"Lambda Function\nProcessEvents","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"MyAPI","x":-495.2115987460815,"y":-83.18260188087775,"label":"API Gateway\nMyAPI","model":{"type":"api","description":""},"group":"api","shadow":false},{"id":"MyIdentity","x":-472.5548589341693,"y":62.467868338558,"label":"Cognito Identity\nMyIdentity","model":{"type":"cognitoIdentity","description":""},"group":"cognitoIdentity","shadow":false}],"edges":[{"from":"MyFirehose","to":"MyBucket","label":"destination","color":{"color":"blue"},"id":"7abe84ac-808b-4d83-a4b8-c0c130f5bd4d"},{"from":"MyFirehose","to":"TransformFirehose","label":"transform","color":{"color":"blue"},"id":"b3d76546-e9f6-4bb4-9714-758330b7282b"},{"from":"ProcessEvents","to":"MyFirehose","label":"put","color":{"color":"green"},"dashes":true,"id":"ec684344-73c9-4671-8370-9e478fc83de1"},{"from":"MyAPI","to":"ProcessEvents","label":"integration","color":{"color":"blue"},"id":"b917a548-e642-4c1b-a506-7ea052e4965f"},{"from":"MyIdentity","to":"MyAPI","label":"authorize","color":{"color":"blue"},"id":"a13a1d54-6781-40c5-91f5-4a16794e6d1f"}]}
--------------------------------------------------------------------------------
/dist/examples/full-app.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "MyIdentity",
5 | "x": -374.203125,
6 | "y": -112,
7 | "label": "Cognito Identity\nMyIdentity",
8 | "title": "My Identity",
9 | "model": {
10 | "type": "cognitoIdentity",
11 | "description": "My Identity"
12 | },
13 | "group": "cognitoIdentity",
14 | "shadow": false
15 | },
16 | {
17 | "id": "MyApi",
18 | "x": -316.203125,
19 | "y": 39,
20 | "label": "API Gateway\nMyApi",
21 | "title": "My REST API",
22 | "model": {
23 | "type": "api",
24 | "description": "My REST API"
25 | },
26 | "group": "api",
27 | "shadow": false
28 | },
29 | {
30 | "id": "MyBackEnd",
31 | "x": -153.203125,
32 | "y": 54,
33 | "label": "Lambda Function\nMyBackEnd",
34 | "title": "My Back End Function",
35 | "model": {
36 | "type": "fn",
37 | "description": "My Back End Function"
38 | },
39 | "group": "fn",
40 | "shadow": false
41 | },
42 | {
43 | "id": "MyTable",
44 | "x": 12.796875,
45 | "y": -126,
46 | "label": "DynamoDB Table\nMyTable",
47 | "title": "My Table",
48 | "model": {
49 | "type": "table",
50 | "description": "My Table"
51 | },
52 | "group": "table",
53 | "shadow": false
54 | },
55 | {
56 | "id": "MyBucket",
57 | "x": 40.796875,
58 | "y": 46,
59 | "label": "S3 Bucket\nMyBucket",
60 | "title": "My Bucket",
61 | "model": {
62 | "type": "bucket",
63 | "description": "My Bucket"
64 | },
65 | "group": "bucket",
66 | "shadow": false
67 | },
68 | {
69 | "id": "MyStream",
70 | "x": 26.796875,
71 | "y": 199,
72 | "label": "Kinesis Stream\nMyStream",
73 | "title": "My Stream",
74 | "model": {
75 | "type": "stream",
76 | "description": "My Stream"
77 | },
78 | "group": "stream",
79 | "shadow": false
80 | },
81 | {
82 | "id": "MyAnalytics",
83 | "x": 142.796875,
84 | "y": 195,
85 | "label": "Kinesis Analytics\nMyAnalytics",
86 | "title": "My Analytics Stream",
87 | "model": {
88 | "type": "analyticsStream",
89 | "description": "My Analytics Stream"
90 | },
91 | "group": "analyticsStream",
92 | "shadow": false
93 | },
94 | {
95 | "id": "MyFirehose",
96 | "x": 257.796875,
97 | "y": 209,
98 | "label": "Kinesis Firehose\nMyFirehose",
99 | "title": "My Firehose Stream",
100 | "model": {
101 | "type": "deliveryStream",
102 | "description": "My Firehose Stream"
103 | },
104 | "group": "deliveryStream",
105 | "shadow": false
106 | },
107 | {
108 | "id": "MyDBProcessor",
109 | "x": 198.61688250277794,
110 | "y": -199.64783765544206,
111 | "label": "Lambda Function\nMyDBProcessor",
112 | "title": "My Database Processor Function",
113 | "model": {
114 | "type": "fn",
115 | "description": "My Database Processor Function"
116 | },
117 | "group": "fn",
118 | "shadow": false
119 | },
120 | {
121 | "id": "MySchedule",
122 | "x": -396.1007836356701,
123 | "y": -203.7255377556265,
124 | "label": "Schedule\nMySchedule",
125 | "title": "My Schedule",
126 | "model": {
127 | "type": "schedule",
128 | "description": "My Schedule"
129 | },
130 | "group": "schedule",
131 | "shadow": false
132 | },
133 | {
134 | "id": "MyTopic",
135 | "x": -372.0039122190005,
136 | "y": -65.71618327833689,
137 | "label": "SNS Topic\nMyTopic",
138 | "title": "My Topic",
139 | "model": {
140 | "type": "topic",
141 | "description": "My Topic"
142 | },
143 | "group": "topic",
144 | "shadow": false
145 | },
146 | {
147 | "id": "MyScheduledActivities",
148 | "x": -433.3414030977959,
149 | "y": -185.10522802456362,
150 | "label": "Lambda Function\nMyScheduledActivities",
151 | "title": "My Scheduled Activities",
152 | "model": {
153 | "type": "fn",
154 | "description": "My Scheduled Activities"
155 | },
156 | "group": "fn",
157 | "shadow": false
158 | },
159 | {
160 | "id": "MyStateMachine",
161 | "x": -820.221039971391,
162 | "y": -176.89564234817388,
163 | "label": "Step Function\nMyStateMachine",
164 | "title": "My State Machine",
165 | "model": {
166 | "type": "stepFn",
167 | "description": "My State Machine"
168 | },
169 | "group": "stepFn",
170 | "shadow": false
171 | }
172 | ],
173 | "edges": [
174 | {
175 | "from": "MyIdentity",
176 | "to": "MyApi",
177 | "label": "authorize",
178 | "color": {"color":"blue"},
179 | "id": "aadc8687-1509-41fa-96b8-1eef0d3f343e"
180 | },
181 | {
182 | "from": "MyApi",
183 | "to": "MyBackEnd",
184 | "label": "integration",
185 | "color": {"color":"blue"},
186 | "id": "064f6cb8-0113-4f1d-ad2a-7c46316671cd"
187 | },
188 | {
189 | "from": "MyBackEnd",
190 | "to": "MyTable",
191 | "label": "read/write",
192 | "color": {"color":"green"},
193 | "dashes": true,
194 | "id": "cfb93b86-400e-4fce-b3cb-7d5874a24372"
195 | },
196 | {
197 | "from": "MyBackEnd",
198 | "to": "MyBucket",
199 | "label": "read/write",
200 | "color": {"color":"green"},
201 | "dashes": true,
202 | "id": "39877ecc-1b3b-4f3a-be7e-27a7eac32753"
203 | },
204 | {
205 | "from": "MyBackEnd",
206 | "to": "MyStream",
207 | "label": "put",
208 | "color": {"color":"green"},
209 | "dashes": true,
210 | "id": "971f5ea3-f5b2-4907-8cd3-6a10c799fbdc"
211 | },
212 | {
213 | "from": "MyStream",
214 | "to": "MyAnalytics",
215 | "label": "input",
216 | "color": {"color":"blue"},
217 | "id": "505b6654-80fd-4ea4-a0b3-c335efe8321b"
218 | },
219 | {
220 | "from": "MyAnalytics",
221 | "to": "MyFirehose",
222 | "label": "output",
223 | "color": {"color":"blue"},
224 | "id": "96cacf24-7571-47cd-9601-8968fdd24216"
225 | },
226 | {
227 | "from": "MyFirehose",
228 | "to": "MyBucket",
229 | "label": "destination",
230 | "color": {"color":"blue"},
231 | "id": "1ae0b94c-014b-4f01-9282-5693f9f757b0"
232 | },
233 | {
234 | "from": "MyTable",
235 | "to": "MyDBProcessor",
236 | "label": "stream",
237 | "color": {"color":"blue"},
238 | "id": "a78e277f-30bc-4a66-8d8e-dca5e6f51391"
239 | },
240 | {
241 | "from": "MyDBProcessor",
242 | "to": "MyBucket",
243 | "label": "read/write",
244 | "color": {"color":"green"},
245 | "dashes": true,
246 | "id": "719bf15e-a4a0-47a5-a4de-448c5d51fc10"
247 | },
248 | {
249 | "from": "MySchedule",
250 | "to": "MyScheduledActivities",
251 | "label": "target",
252 | "color": {"color":"blue"},
253 | "id": "3bee92fd-f2b7-4cc0-a6da-b87f5e73eca4"
254 | },
255 | {
256 | "from": "MyScheduledActivities",
257 | "to": "MyTable",
258 | "label": "read/write",
259 | "color": {"color":"green"},
260 | "dashes": true,
261 | "id": "6979d2a2-54c9-4dd8-b69d-04e07a8c4274"
262 | },
263 | {
264 | "from": "MyTopic",
265 | "to": "MyScheduledActivities",
266 | "label": "trigger",
267 | "color": {"color":"blue"},
268 | "id": "fc9492bd-6de5-4888-898a-2ff04c1a0c6c"
269 | },
270 | {
271 | "from": "MyScheduledActivities",
272 | "to": "MyStateMachine",
273 | "label": "activity",
274 | "color": {"color":"green"},
275 | "dashes": true,
276 | "id": "ab9c21ef-5dc7-49ed-84ba-e2b67f3ce962"
277 | },
278 | {
279 | "from": "MyStateMachine",
280 | "to": "MyBackEnd",
281 | "label": "invoke",
282 | "color": {"color":"blue"},
283 | "id": "698e9074-e122-4d78-a91d-4b2eb593dfbc"
284 | }
285 | ]
286 | }
--------------------------------------------------------------------------------
/dist/examples/iot.json:
--------------------------------------------------------------------------------
1 | {"nodes":[{"id":"i1","x":-314,"y":-184.5,"label":"IoT Topic Rule\ni1","model":{"type":"iotRule","description":""},"group":"iotRule","shadow":false},{"id":"f1","x":-67,"y":-156.5,"label":"Lambda Function\nf1","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"b1","x":120,"y":-127.5,"label":"S3 Bucket\nb1","model":{"type":"bucket","description":""},"group":"bucket","shadow":false}],"edges":[{"from":"i1","to":"f1","label":"invoke","color":{"color":"blue"},"id":"68ce9cac-8a9a-4553-94dd-af56457b5cfc"},{"from":"f1","to":"b1","label":"read/write","color":{"color":"green"},"dashes":true,"id":"67986134-9fd3-40bd-9cb9-a46b30ef121e"},{"from":"i1","to":"i1","label":"republish","color":{"color":"blue"},"id":"d6753dda-23e4-400c-be53-15df9c8269cd"}]}
--------------------------------------------------------------------------------
/dist/examples/s3-processing.json:
--------------------------------------------------------------------------------
1 | {"nodes":[{"id":"SourceBucket","x":-457,"y":-64.5,"label":"S3 Bucket\nSourceBucket","title":"The Source Bucket","model":{"type":"bucket","description":"The Source Bucket"},"group":"bucket","shadow":false},{"id":"DestBucket","x":-68,"y":-62.5,"label":"S3 Bucket\nDestBucket","title":"The Destination Bucket","model":{"type":"bucket","description":"The Destination Bucket"},"group":"bucket","shadow":false},{"id":"DataProc","x":-284,"y":-84.5,"label":"Lambda Function\nDataProc","title":"The Data Processing function","model":{"type":"fn","description":"The Data Processing function"},"group":"fn","shadow":false}],"edges":[{"from":"SourceBucket","to":"DataProc","label":"trigger","color":{"color":"blue"},"id":"18a2c25d-446c-4c38-9a6a-65d2fe135d36"},{"from":"DataProc","to":"DestBucket","label":"read/write","color":{"color":"green"},"dashes":true,"id":"aa602462-a8d9-4b92-a307-71a665cc924a"}]}
--------------------------------------------------------------------------------
/dist/examples/stream-test.json:
--------------------------------------------------------------------------------
1 | {"nodes":[{"id":"api","x":-228.203125,"y":-154.5,"label":"API Gateway\napi","model":{"type":"api","description":""},"group":"api","shadow":false},{"id":"f1","x":-41.203125,"y":-40.5,"label":"Lambda Function\nf1","model":{"type":"fn","description":""},"group":"fn","shadow":false},{"id":"ks","x":128.796875,"y":12.5,"label":"Kinesis Stream\nks","model":{"type":"stream","description":""},"group":"stream","shadow":false},{"id":"ka","x":254.796875,"y":14.5,"label":"Kinesis Analytics\nka","model":{"type":"analyticsStream","description":""},"group":"analyticsStream","shadow":false},{"id":"kf","x":372.796875,"y":26.5,"label":"Kinesis Firehose\nkf","model":{"type":"deliveryStream","description":""},"group":"deliveryStream","shadow":false},{"id":"b1","x":-220.203125,"y":131.5,"label":"S3 Bucket\nb1","model":{"type":"bucket","description":""},"group":"bucket","shadow":false}],"edges":[{"from":"api","to":"f1","label":"integration","color":{"color":"blue"},"id":"efc8a873-8adc-474c-b94f-edd668cbe6ed"},{"from":"f1","to":"ks","label":"put","color":{"color":"green"},"dashes":true,"id":"1facaaa6-5777-4047-9501-02d319b9748a"},{"from":"ks","to":"ka","label":"input","color":{"color":"blue"},"id":"4593fba5-0f35-4d41-9f31-8be61690e4ef"},{"from":"ka","to":"kf","label":"output","color":{"color":"blue"},"id":"78bb14a7-2b39-4ebd-a248-01f4b3039e50"},{"from":"kf","to":"b1","label":"destination","color":{"color":"blue"},"id":"bac5a755-72e3-4f45-8c4e-f120d435fdaf"}]}
--------------------------------------------------------------------------------
/dist/img/aws/AWS-Lambda_Function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/AWS-Lambda_Function.png
--------------------------------------------------------------------------------
/dist/img/aws/AWS-Step-Functions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/AWS-Step-Functions.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-API-Gateway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-API-Gateway.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-CloudWatch_Event-Time-Based.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-CloudWatch_Event-Time-Based.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-Cognito.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Cognito.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-DynamoDB_Table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-DynamoDB_Table.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-Kinesis-Data-Analytics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Analytics.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-Kinesis-Data-Firehose.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Firehose.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-Kinesis-Data-Streams.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-Kinesis-Data-Streams.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-S3_Bucket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-S3_Bucket.png
--------------------------------------------------------------------------------
/dist/img/aws/Amazon-SNS_Topic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/Amazon-SNS_Topic.png
--------------------------------------------------------------------------------
/dist/img/aws/IoT_Rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/aws/IoT_Rule.png
--------------------------------------------------------------------------------
/dist/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danilop/ServerlessByDesign/dfd4ee94669f93d3846db0593e9840e6137bbdf8/dist/img/logo.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Serverless by Design
6 |
7 |
8 |
9 |
10 |
11 |
12 |
47 |
48 |
49 |
50 |
51 |
52 |
106 |
107 |
108 |
109 |
148 |
149 |
150 |
151 |
152 |
158 |
159 |
160 |
161 | Please paste the JSON data here
162 |
163 |
164 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
183 |
184 |
Welcome to
185 | Serverless by Design - Think. Build. Repeat.
186 |
187 | An application is a network of
188 | nodes (serverless resources, such as Lambda functions or S3 buckets) connected by
189 | edges (their relationships, for example a trigger or a data flow)
190 |
191 | Edit an application adding nodes and edges following an
192 | event-driven design
193 |
194 | Import a previously exported application to continue working on it
195 | Choose a
196 | runtime , and
197 | build your application (for example, using AWS SAM)
198 | Optionally use canary or linear deployments for your future updates
199 | Edit
200 | templates and code files for the final configuratons before deploying the application
201 |
202 | Export an application to save it for later use in a JSON file
203 | Take a
204 | picture of the application architecture to have a visual representation to share
205 | Fine tune the
206 | physics used to place nodes and edges on the screen, for example enable/disable it
207 | or choose another solver
208 |
209 |
Here are a few examples to help you start:
210 |
224 |
225 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
238 |
239 |
241 |
243 |
244 |
245 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "new-repo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "webpack --devtool source-map",
9 | "build": "webpack -p"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "css-loader": "^2.1.1",
16 | "file-loader": "^3.0.1",
17 | "image-webpack-loader": "^4.6.0",
18 | "style-loader": "^0.23.1",
19 | "webpack": "^4.29.6",
20 | "webpack-cli": "^3.3.2"
21 | },
22 | "dependencies": {
23 | "bootstrap": "^4.3.1",
24 | "file-saver": "^2.0.1",
25 | "jquery": ">=3.4.0",
26 | "js-yaml": "^3.13.0",
27 | "jszip": "^3.2.1",
28 | "popper.js": "^1.15.0",
29 | "vis": "^4.21.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/engines/sam.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const jsyaml = require('js-yaml');
4 |
5 | const runtimes = {
6 | "nodejs8.10": {
7 | fileExtension: "js",
8 | handler: "handler",
9 | startingCode:
10 | `'use strict';
11 |
12 | console.log("Loading function");
13 |
14 | exports.handler = async (event, context) => {
15 | console.log('Received event:', JSON.stringify(event, null, 2));
16 | return "Hello World";
17 | // or
18 | // throw new Error(“some error type”);
19 | };
20 |
21 | `
22 | },
23 | "python3.7": {
24 | fileExtension: "py",
25 | handler: "lambda_handler",
26 | startingCode:
27 | `import json
28 |
29 | print("Loading function")
30 |
31 | def lambda_handler(event, context):
32 | print("Received event: " + json.dumps(event, indent=2))
33 | return "Hello World"
34 | or
35 | #raise Exception("Something went wrong")
36 | `
37 | }
38 | }
39 |
40 | var renderingRules = {
41 | bucket: {
42 | resource: function (status, node) {
43 | status.template.Resources[node.id] = {
44 | Type: "AWS::S3::Bucket"
45 | }
46 | },
47 | event: function (status, id, idFrom) {
48 | status.template.Resources[id].Properties.Events['Bucket' + idFrom] = {
49 | Type: "S3",
50 | Properties: {
51 | Bucket: "!Ref " + idFrom,
52 | Events: "s3:ObjectCreated:*"
53 | }
54 | };
55 | // To avoid circular dependencies with a more specific policy
56 | status.template.Resources[id].Properties.Policies.push('AmazonS3ReadOnlyAccess');
57 | },
58 | policy: function (status, id, idTo) {
59 | return {
60 | Effect: "Allow",
61 | Action: ["s3:GetObject", "s3:PutObject"],
62 | Resource: "!Sub ${" + idTo + ".Arn}/*"
63 | };
64 | },
65 | },
66 | table: {
67 | resource: function (status, node) {
68 | status.template.Resources[node.id] = {
69 | Type: "AWS::DynamoDB::Table",
70 | Properties: {
71 | AttributeDefinitions: [
72 | {
73 | AttributeName: "id",
74 | AttributeType: "S"
75 | },
76 | {
77 | AttributeName: "version",
78 | AttributeType: "N"
79 | }
80 | ],
81 | KeySchema: [
82 | {
83 | AttributeName: "id",
84 | KeyType: "HASH"
85 | },
86 | {
87 | AttributeName: "version",
88 | KeyType: "RANGE"
89 | }
90 | ],
91 | BillingMode: 'PAY_PER_REQUEST',
92 | StreamSpecification: {
93 | StreamViewType: "NEW_AND_OLD_IMAGES"
94 | }
95 | }
96 | };
97 | },
98 | event: function (status, id, idFrom) {
99 | status.template.Resources[id].Properties.Events['Table' + idFrom] = {
100 | Type: "DynamoDB",
101 | Properties: {
102 | Stream: "!GetAtt " + idFrom + ".StreamArn",
103 | StartingPosition: "TRIM_HORIZON",
104 | BatchSize: 10
105 | }
106 | };
107 | },
108 | policy: function (status, id, idTo) {
109 | return {
110 | Effect: "Allow",
111 | Action: ["dynamodb:GetItem", "dynamodb:PutItem"],
112 | Resource: "!GetAtt " + idTo + ".Arn"
113 | };
114 | },
115 | },
116 | api: {
117 | resource: function (status, node) {
118 | // Nothing to do, created by the API event
119 | },
120 | event: function (status, id, idFrom) {
121 | status.template.Resources[id].Properties.Events['Api' + idFrom] = {
122 | Type: "Api",
123 | Properties: {
124 | Path: "/{proxy+}",
125 | Method: "ANY"
126 | }
127 | };
128 | },
129 | policy: function (status, id, idTo) {
130 | return {
131 | Effect: "Allow",
132 | Action: "execute-api:Invoke",
133 | Resource: "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:*/*/*/*"
134 | };
135 | },
136 | },
137 | stream: {
138 | resource: function (status, node) {
139 | status.template.Resources[node.id] = {
140 | Type: "AWS::Kinesis::Stream",
141 | Properties: {
142 | ShardCount: 1
143 | }
144 | };
145 | },
146 | event: function (status, id, idFrom) {
147 | status.template.Resources[id].Properties.Events['Stream' + idFrom] = {
148 | Type: "Kinesis",
149 | Properties: {
150 | Stream: "!GetAtt " + idFrom + ".Arn",
151 | StartingPosition: "TRIM_HORIZON",
152 | BatchSize: 10
153 | }
154 | };
155 | },
156 | policy: function (status, id, idTo) {
157 | return {
158 | Effect: "Allow",
159 | Action: ["kinesis:PutRecord", "kinesis:PutRecords"],
160 | Resource: "!GetAtt " + idTo + ".Arn"
161 | };
162 | },
163 | },
164 | deliveryStream: {
165 | resource: function (status, node) {
166 | var targetBucketId = null;
167 | var targetFnId = null;
168 | node.to.forEach(function (idTo) { // Target resources
169 | var node_to = status.model.nodes[idTo];
170 | if (node_to.type === 'bucket') {
171 | targetBucketId = idTo;
172 | } else if (node_to.type === 'fn') {
173 | targetFnId = idTo;
174 | }
175 | });
176 | if (targetBucketId == null) {
177 | console.error("Delivery Stream without a destination");
178 | return;
179 | }
180 | var deliveryPolicyId = node.id + "DeliveryPolicy";
181 | var deliveryRoleId = node.id + "DeliveryRole";
182 | // Create the Delivery Strem
183 | status.template.Resources[node.id] = {
184 | DependsOn: [deliveryPolicyId],
185 | Type: 'AWS::KinesisFirehose::DeliveryStream',
186 | Properties: {
187 | ExtendedS3DestinationConfiguration: {
188 | BucketARN: "!GetAtt " + targetBucketId + ".Arn",
189 | BufferingHints: {
190 | IntervalInSeconds: 60,
191 | SizeInMBs: 50
192 | },
193 | CompressionFormat: "UNCOMPRESSED",
194 | Prefix: "firehose/",
195 | RoleARN: "!GetAtt " + deliveryRoleId + ".Arn"
196 | }
197 | }
198 | };
199 | if (targetFnId !== null) {
200 | status.template.Resources[node.id].Properties
201 | .ExtendedS3DestinationConfiguration.ProcessingConfiguration = {
202 | Enabled: true,
203 | Processors: [{
204 | Parameters: [{
205 | ParameterName: "LambdaArn",
206 | ParameterValue: "!GetAtt " + targetFnId + ".Arn"
207 | }],
208 | Type: "Lambda"
209 | }]
210 | }
211 | }
212 | // Create a delivery role
213 | status.template.Resources[deliveryRoleId] = {
214 | Type: 'AWS::IAM::Role',
215 | Properties: {
216 | AssumeRolePolicyDocument: {
217 | Version: '2012-10-17',
218 | Statement: [{
219 | Effect: "Allow",
220 | Principal: { Service: "firehose.amazonaws.com" },
221 | Action: 'sts:AssumeRole',
222 | Condition: {
223 | StringEquals: {
224 | 'sts:ExternalId': "!Ref AWS::AccountId"
225 | }
226 | }
227 | }]
228 | }
229 | }
230 | };
231 | // Create a delivery policy for the role
232 | status.template.Resources[deliveryPolicyId] = {
233 | Type: 'AWS::IAM::Policy',
234 | Properties: {
235 | PolicyName: "firehose_delivery_policy",
236 | PolicyDocument: {
237 | Version: '2012-10-17',
238 | Statement: [{
239 | Effect: 'Allow',
240 | Action: [
241 | 's3:AbortMultipartUpload',
242 | 's3:GetBucketLocation',
243 | 's3:GetObject',
244 | 's3:ListBucket',
245 | 's3:ListBucketMultipartUploads',
246 | 's3:PutObject'
247 | ],
248 | Resource: [
249 | "!GetAtt " + targetBucketId + ".Arn"
250 | ]
251 | }]
252 | },
253 | Roles: ["!Ref " + deliveryRoleId]
254 | }
255 | };
256 | },
257 | event: function () { }, // TODO
258 | policy: function (status, id, idTo) {
259 | return {
260 | Effect: "Allow",
261 | Action: [
262 | "firehose:PutRecord",
263 | "firehose:PutRecordBatch"
264 | ],
265 | // Kinesis Firehose ARN syntax (can't use GetAtt)
266 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
267 | Resource: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + idTo + "}",
268 | };
269 | },
270 | },
271 | analyticsStream: {
272 | resource: function (status, node) {
273 |
274 | // Input resources
275 | var inputStreamId = null;
276 | var inputDeliveryStreamId = null;
277 | node.from.forEach(function (idFrom) {
278 | var node_from = status.model.nodes[idFrom];
279 | if (node_from.type === 'stream') {
280 | inputStreamId = idFrom;
281 | } else if (node_from.type === 'deliveryStream') {
282 | inputDeliveryStreamId = idFrom;
283 | }
284 | });
285 |
286 | // Output resource
287 | var outputStreamId = null;
288 | var outputDeliveryStreamId = null;
289 | node.to.forEach(function (idTo) {
290 | var node_to = status.model.nodes[idTo];
291 | if (node_to.type === 'stream') {
292 | outputStreamId = idTo;
293 | } else if (node_to.type === 'deliveryStream') {
294 | outputDeliveryStreamId = idTo;
295 | }
296 | });
297 |
298 | var analyticsStreamRoleId = node.id + "Role";
299 | var analyticsStreamOutputId = node.id + "Outputs";
300 |
301 | status.template.Resources[node.id] = {
302 | Type: "AWS::KinesisAnalytics::Application",
303 | Properties: {
304 | ApplicationName: node.id,
305 | Inputs: [{
306 | NamePrefix: "exampleNamePrefix",
307 | InputSchema: {
308 | RecordColumns: [{
309 | Name: "example",
310 | SqlType: "VARCHAR(16)",
311 | Mapping: "$.example"
312 | }],
313 | RecordFormat: {
314 | RecordFormatType: "JSON",
315 | MappingParameters: {
316 | JSONMappingParameters: {
317 | RecordRowPath: "$"
318 | }
319 | }
320 | }
321 | }
322 | }]
323 | }
324 | };
325 |
326 | if (node.description !== '') {
327 | status.template.Resources[node.id]
328 | .Properties.ApplicationDescription = node.description;
329 | }
330 |
331 | if (inputStreamId !== null) {
332 | status.template.Resources[node.id]
333 | .Properties.Inputs[0].KinesisStreamsInput = {
334 | ResourceARN: "!GetAtt " + inputStreamId + ".Arn",
335 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn"
336 | };
337 | }
338 |
339 | if (inputDeliveryStreamId !== null) {
340 | status.template.Resources[node.id]
341 | .Properties.Inputs[0].KinesisFirehoseInput = {
342 | // Kinesis Firehose ARN syntax (can't use GetAtt)
343 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
344 | ResourceARN: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + inputDeliveryStreamId + "}",
345 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn"
346 | };
347 | }
348 |
349 | status.template.Resources[analyticsStreamRoleId] = {
350 | Type: "AWS::IAM::Role",
351 | Properties: {
352 | AssumeRolePolicyDocument: {
353 | Version: "2012-10-17",
354 | Statement: [{
355 | Effect: "Allow",
356 | Principal: {
357 | Service: "kinesisanalytics.amazonaws.com"
358 | },
359 | Action: "sts:AssumeRole"
360 | }]
361 | },
362 | Path: "/",
363 | Policies: [{
364 | PolicyName: "Open",
365 | PolicyDocument: {
366 | Version: "2012-10-17",
367 | Statement: [{
368 | Effect: "Allow",
369 | Action: "*",
370 | Resource: "*"
371 | }]
372 | }
373 | }]
374 | }
375 | };
376 |
377 | status.template.Resources[analyticsStreamOutputId] = {
378 | Type: "AWS::KinesisAnalytics::ApplicationOutput",
379 | DependsOn: node.id,
380 | Properties: {
381 | ApplicationName: "!Ref " + node.id,
382 | Output: {
383 | Name: "exampleOutput",
384 | DestinationSchema: {
385 | RecordFormatType: "CSV"
386 | }
387 | }
388 | }
389 | };
390 |
391 | if (outputStreamId !== null) {
392 | status.template.Resources[analyticsStreamOutputId]
393 | .Properties.Output.KinesisStreamsOutput = {
394 | ResourceARN: "!GetAtt " + outputStreamId + ".Arn",
395 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn"
396 | };
397 | }
398 |
399 | if (outputDeliveryStreamId !== null) {
400 | status.template.Resources[analyticsStreamOutputId]
401 | .Properties.Output.KinesisFirehoseOutput = {
402 | // Kinesis Firehose ARN syntax (can't use GetAtt)
403 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
404 | ResourceARN: "!Sub arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:deliverystream/${" + outputDeliveryStreamId + "}",
405 | RoleARN: "!GetAtt " + analyticsStreamRoleId + ".Arn"
406 | };
407 | }
408 |
409 | },
410 | event: function () { }, // TODO
411 | policy: function () { } // TODO
412 | },
413 | schedule: {
414 | resource: function (status, node) {
415 | // Nothing to do
416 | },
417 | event: function (status, id, idFrom) {
418 | status.template.Resources[id].Properties.Events['Schedule' + idFrom] = {
419 | Type: "Schedule",
420 | Properties: {
421 | Schedule: "rate(5 minutes)"
422 | }
423 | };
424 | },
425 | policy: function () { } // This has no sense
426 | },
427 | topic: {
428 | resource: function (status, node) {
429 | status.template.Resources[node.id] = {
430 | Type: "AWS::SNS::Topic"
431 | };
432 | },
433 | event: function (status, id, idFrom) {
434 | status.template.Resources[id].Properties.Events['Topic' + idFrom] = {
435 | Type: "SNS",
436 | Properties: {
437 | Topic: "!Ref " + idFrom
438 | }
439 | };
440 | },
441 | policy: function (status, id, idTo) {
442 | return {
443 | Effect: "Allow",
444 | Action: "sns:Publish",
445 | Resource: "!Ref " + idTo // For an SNS topic, it returns the ARN
446 | };
447 | },
448 | },
449 | fn: {
450 | resource: function (status, node) {
451 | status.template.Resources[node.id] = {
452 | Type: "AWS::Serverless::Function",
453 | Properties: {
454 | // FunctionName: node.id,
455 | Handler: node.id + "." + runtimes[status.runtime].handler,
456 | Runtime: status.runtime,
457 | CodeUri: ".",
458 | Policies: []
459 | }
460 | };
461 | if (node.description !== '') {
462 | status.template.Resources[node.id].Properties.Description = node.description;
463 | }
464 | status.files[node.id + '.' + runtimes[status.runtime].fileExtension] =
465 | runtimes[status.runtime].startingCode;
466 | if (node.from.length > 0) { // There are triggers for this function
467 | status.template.Resources[node.id].Properties.Events = {};
468 | node.from.forEach(function (idFrom) {
469 | console.log("Trigger " + idFrom + " -> " + node.id);
470 | renderingRules[status.model.nodes[idFrom].type].event(status, node.id, idFrom);
471 | });
472 | }
473 | if (node.to.length > 0) { // There are resources target of this function
474 | var policy = {
475 | Version: "2012-10-17",
476 | Statement: []
477 | };
478 | node.to.forEach(function (idTo) {
479 | console.log("Policy " + node.id + " -> " + idTo);
480 | policy.Statement.push(
481 | renderingRules[status.model.nodes[idTo].type]
482 | .policy(status, node.id, idTo)
483 | );
484 | });
485 | status.template.Resources[node.id].Properties.Policies.push(policy);
486 | }
487 | },
488 | event: function () { }, // Nothing to do, this is not a trigger, but a fn to fn invocation
489 | policy: function (status, id, idTo) {
490 | return {
491 | Effect: "Allow",
492 | Action: ["lambda:Invoke", "lambda:InvokeAsync"],
493 | Resource: "!GetAtt " + idTo + ".Arn"
494 | };
495 | }
496 | },
497 | stepFn: {
498 | resource: function (status, node) {
499 | status.template.Resources[node.id] = {
500 | Type: "AWS::StepFunctions::StateMachine",
501 | Properties: {
502 | // The DefinitionString is added later
503 | // This role is automatically created by the AWS console
504 | // the first time you create a state machine in a region
505 | RoleArn: "!Sub arn:aws:iam::${AWS::AccountId}:role/service-role/StatesExecutionRole-${AWS::Region}"
506 | },
507 | };
508 | var definitionString = {
509 | Comment: "A Hello World example",
510 | StartAt: "HelloWorld",
511 | States: {
512 | HelloWorld: {
513 | Type: "Pass",
514 | Result: "Hello World!",
515 | End: true
516 | }
517 | }
518 | };
519 | // The DefinitionString must be a string with JSON syntax within the template
520 | status.template.Resources[node.id].Properties.DefinitionString =
521 | JSON.stringify(definitionString, null, 2);
522 | },
523 | event: function () { }, // Nothing to do
524 | policy: function (status, id, idTo) {
525 | return {
526 | "Effect": "Allow",
527 | "Action": [
528 | "states:DescribeExecution",
529 | "states:GetExecutionHistory",
530 | "states:ListExecutions",
531 | "states:StartExecution",
532 | "states:StopExecution"
533 | ],
534 | "Resource": [
535 | "!Ref " + idTo
536 | ]
537 | }
538 | }
539 | },
540 | cognitoIdentity: {
541 | resource: function (status, node) {
542 | var cognitoUnauthRoleId = node.id + "CognitoUnauthRole";
543 | var cognitoUnauthPolicyId = node.id + "CognitoUnauthPolicy";
544 | status.template.Resources[node.id] = {
545 | Type: "AWS::Cognito::IdentityPool",
546 | Properties: {
547 | AllowUnauthenticatedIdentities: true // TODO Maybe this is not a secure default ???
548 | }
549 | }
550 | status.template.Resources[cognitoUnauthRoleId] = {
551 | Type: 'AWS::IAM::Role',
552 | Properties: {
553 | AssumeRolePolicyDocument: {
554 | Version: '2012-10-17',
555 | Statement: [{
556 | Effect: "Allow",
557 | Principal: { Federated: "cognito-identity.amazonaws.com" },
558 | Action: 'sts:AssumeRoleWithWebIdentity',
559 | Condition: {
560 | StringEquals: {
561 | "cognito-identity.amazonaws.com:aud": "!Ref " + node.id
562 | },
563 | "ForAnyValue:StringLike": {
564 | "cognito-identity.amazonaws.com:amr": "unauthenticated"
565 | }
566 | }
567 | }]
568 | }
569 | }
570 | }
571 | // Create a delivery policy for the role
572 | status.template.Resources[cognitoUnauthPolicyId] = {
573 | Type: 'AWS::IAM::Policy',
574 | Properties: {
575 | PolicyName: "cognito_unauth_policy",
576 | PolicyDocument: {
577 | Version: '2012-10-17',
578 | Statement: []
579 | },
580 | Roles: ["!Ref " + cognitoUnauthRoleId]
581 | }
582 | };
583 | // Output resources
584 | node.to.forEach(function (idTo) {
585 | var node_to = status.model.nodes[idTo];
586 | status.template.Resources[cognitoUnauthPolicyId]
587 | .Properties.PolicyDocument.Statement.push(
588 | renderingRules[status.model.nodes[idTo].type]
589 | .policy(status, node.id, idTo)
590 | );
591 | });
592 | },
593 | event: function () { }, // TODO ???
594 | policy: function () { } // TODO ???
595 | },
596 | iotRule: {
597 | resource: function (status, node) {
598 | status.template.Resources[node.id] = {
599 | Type: "AWS::IoT::TopicRule",
600 | Properties: {
601 | TopicRulePayload: {
602 | RuleDisabled: "true", // safe choice
603 | Sql: "Select temp FROM 'Some/Topic' WHERE temp > 60",
604 | Actions: []
605 | }
606 | }
607 | }
608 | if (node.description !== '') {
609 | status.template.Resources[node.id].Properties.TopicRulePayload.Description = node.description;
610 | }
611 | // Output resources
612 | node.to.forEach(function (idTo) {
613 | var node_to = status.model.nodes[idTo];
614 | switch (node_to.type) {
615 | case 'fn':
616 | status.template.Resources[node.id].Properties.TopicRulePayload.Actions.push({
617 | Lambda: {
618 | FunctionArn: "!GetAtt " + idTo + ".Arn"
619 | }
620 | });
621 | break;
622 | case 'iotRule': // republish
623 | var republishRoleId = idTo + "PublishRole";
624 | status.template.Resources[node.id].Properties.TopicRulePayload.Actions.push({
625 | Republish: {
626 | Topic: "Output/Topic",
627 | RoleArn: "!GetAtt " + republishRoleId + ".Arn"
628 | }
629 | });
630 | status.template.Resources[republishRoleId] = {
631 | Type: "AWS::IAM::Role",
632 | Properties: {
633 | AssumeRolePolicyDocument: {
634 | Version: "2012-10-17",
635 | Statement: [{
636 | Effect: "Allow",
637 | Action: [ "sts:AssumeRole" ],
638 | Principal: {
639 | Service: [ "iot.amazonaws.com" ]
640 | }
641 | }]
642 | },
643 | Policies: [{
644 | PolicyName: "publish",
645 | PolicyDocument: {
646 | Version: "2012-10-17",
647 | Statement: [{
648 | Effect: "Allow",
649 | Action: "iot:Publish",
650 | Resource: "!Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/Output/*"
651 | }]
652 | }
653 | }]
654 | }
655 | };
656 | break;
657 | default:
658 | throw "Error: connection type not supported (" + node_to.type + ")";
659 | }
660 | });
661 | },
662 | event: function () { },
663 | policy: function () { }
664 | }
665 | };
666 |
667 | function render(model, runtime, deployment) {
668 | console.log('Using SAM...');
669 | var files = {};
670 | var template = {
671 | AWSTemplateFormatVersion: "2010-09-09",
672 | Transform: "AWS::Serverless-2016-10-31"
673 | };
674 |
675 | if (deployment) {
676 | template.Globals = {
677 | Function: {
678 | AutoPublishAlias: "live",
679 | DeploymentPreference: {
680 | Type: deployment
681 | }
682 | }
683 | }
684 | }
685 |
686 | var status = {
687 | model: model,
688 | runtime: runtime,
689 | files: files,
690 | template: template
691 | }
692 |
693 | template.Resources = {};
694 | for (var id in model.nodes) {
695 | var node = model.nodes[id];
696 | renderingRules[node.type].resource(status, node);
697 | }
698 |
699 | console.log(template); // Still in JSON
700 | console.log(JSON.stringify(template, null, 4)); // JSON -> text
701 |
702 | for (var r in template.Resources) {
703 | console.log(r + " -> YAML");
704 | console.log(jsyaml.safeDump(template.Resources[r], { lineWidth: 1024 }));
705 | }
706 |
707 | // Line breaks can introduce YAML syntax (e.g. >-) that will put some variables
708 | // (e.g. AWS::Region) between quotes.
709 | // Single quotes must be removed for functions (e.g. !Ref) to work.
710 | files['template.yaml'] = jsyaml.safeDump(template, { lineWidth: 1024 }).replace(/'(!.+)'/g, "$1");
711 |
712 | return files;
713 | }
714 |
715 | module.exports = render;
--------------------------------------------------------------------------------
/src/engines/servfrmwk.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const jsyaml = require('js-yaml');
4 |
5 | const runtimes = {
6 | "nodejs8.10": {
7 | fileExtension: "js",
8 | gitignore: `# package directories
9 | node_modules
10 | jspm_packages
11 |
12 | # Serverless directories
13 | .serverless
14 | `,
15 | handler: "handler",
16 | startingCode:
17 | `'use strict';
18 |
19 | module.exports.handler = (event, context, callback) => {
20 | const response = {
21 | statusCode: 200,
22 | body: JSON.stringify({
23 | message: 'Go Serverless v1.0! Your function executed successfully!',
24 | input: event,
25 | }),
26 | };
27 |
28 | callback(null, response);
29 |
30 | // Use this code if you don't use the http event with the LAMBDA-PROXY integration
31 | // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
32 | };
33 | `
34 | },
35 | "python3.7": {
36 | fileExtension: "py",
37 | gitignore: `# Distribution / packaging
38 | .Python
39 | env/
40 | build/
41 | develop-eggs/
42 | dist/
43 | downloads/
44 | eggs/
45 | .eggs/
46 | lib/
47 | lib64/
48 | parts/
49 | sdist/
50 | var/
51 | *.egg-info/
52 | .installed.cfg
53 | *.egg
54 |
55 | # Serverless directories
56 | .serverless
57 | `,
58 | handler: "handler",
59 | startingCode:
60 | `import json
61 |
62 | def handler(event, context):
63 | body = {
64 | "message": "Go Serverless v1.0! Your function executed successfully!",
65 | "input": event
66 | }
67 |
68 | response = {
69 | "statusCode": 200,
70 | "body": json.dumps(body)
71 | }
72 |
73 | return response
74 |
75 | # Use this code if you don't use the http event with the LAMBDA-PROXY integration
76 | """
77 | return {
78 | "message": "Go Serverless v1.0! Your function executed successfully!",
79 | "event": event
80 | }
81 | """
82 | `
83 | }
84 | };
85 |
86 | var renderingRules = {
87 | bucket: {
88 | resource: function (status, node) {
89 | status.template.resources.Resources[node.id] = {
90 | Type: "AWS::S3::Bucket"
91 | };
92 | },
93 | event: function (status, id, idFrom) {
94 | if (status.model.nodes[id].type === 'fn') {
95 | status.template.functions[id].events.push({
96 | s3: {
97 | bucket: idFrom,
98 | event: "s3:ObjectCreated:*"
99 | }
100 | });
101 | } else {
102 | status.template.resources.Resources[id].Properties.Events['Bucket' + idFrom] = {
103 | Type: "S3",
104 | Properties: {
105 | Bucket: { Ref: idFrom },
106 | Events: "s3:ObjectCreated:*"
107 | }
108 | };
109 |
110 | // To avoid circular dependencies with a more specific policy
111 | status.template.resources.Resources[id].Properties.Policies.push('AmazonS3ReadOnlyAccess');
112 | }
113 | },
114 | policy: function (status, id, idTo) {
115 | return {
116 | Effect: "Allow",
117 | Action: ["s3:GetObject", "s3:PutObject"],
118 | Resource: {
119 | "Fn::Join": [
120 | "",
121 | [
122 | { "Fn::GetAtt": [idTo, "Arn"] },
123 | "/*"
124 | ]
125 | ]
126 | }
127 | };
128 | },
129 | },
130 | table: {
131 | resource: function (status, node) {
132 | status.template.resources.Resources[node.id] = {
133 | Type: "AWS::DynamoDB::Table",
134 | Properties: {
135 | AttributeDefinitions: [
136 | {
137 | AttributeName: "id",
138 | AttributeType: "S"
139 | },
140 | {
141 | AttributeName: "version",
142 | AttributeType: "N"
143 | }
144 | ],
145 | KeySchema: [
146 | {
147 | AttributeName: "id",
148 | KeyType: "HASH"
149 | },
150 | {
151 | AttributeName: "version",
152 | KeyType: "RANGE"
153 | }
154 | ],
155 | BillingMode: 'PAY_PER_REQUEST',
156 | StreamSpecification: {
157 | StreamViewType: "NEW_AND_OLD_IMAGES"
158 | }
159 | }
160 | };
161 | },
162 | event: function (status, id, idFrom) {
163 | if (status.model.nodes[id].type === 'fn') {
164 | status.template.functions[id].events.push({
165 | stream: {
166 | type: "dynamodb",
167 | arn: { "Fn::GetAtt": [idFrom, "StreamArn"] }
168 | }
169 | });
170 | } else {
171 | status.template.resources.Resources[id].Properties.Events['Table' + idFrom] = {
172 | Type: "DynamoDB",
173 | Properties: {
174 | Stream: { "Fn::GetAtt": [idFrom, "StreamArn"] },
175 | StartingPosition: "TRIM_HORIZON",
176 | BatchSize: 10
177 | }
178 | };
179 | }
180 | },
181 | policy: function (status, id, idTo) {
182 | return {
183 | Effect: "Allow",
184 | Action: ["dynamodb:GetItem", "dynamodb:PutItem"],
185 | Resource: { "Fn::GetAtt": [idTo, "Arn"] }
186 | };
187 | },
188 | },
189 | api: {
190 | resource: function (status, node) {
191 | // Nothing to do, created by the API event
192 | },
193 | event: function (status, id, idFrom) {
194 | if (status.model.nodes[id].type === 'fn') {
195 | status.template.functions[id].events.push({
196 | http: {
197 | path: "/{proxy+}",
198 | method: "get"
199 | }
200 | });
201 | } else { // Currently this doesn't execute but could if Step Functions handled events
202 | status.template.resources.Resources[id].Properties.Events['Api' + idFrom] = {
203 | Type: "Api",
204 | Properties: {
205 | Path: "/{proxy+}",
206 | Method: "ANY"
207 | }
208 | };
209 | }
210 | },
211 | policy: function (status, id, idTo) {
212 | return {
213 | Effect: "Allow",
214 | Action: "execute-api:Invoke",
215 | Resource: {
216 | "Fn::Join": [
217 | "",
218 | [
219 | "arn:aws:execute-api:",
220 | { Ref: "AWS::Region" },
221 | ":",
222 | { Ref: "AWS::AccountId" },
223 | ":*/*/*/*"
224 | ]
225 | ]
226 | }
227 | };
228 | },
229 | },
230 | stream: {
231 | resource: function (status, node) {
232 | status.template.resources.Resources[node.id] = {
233 | Type: "AWS::Kinesis::Stream",
234 | Properties: {
235 | ShardCount: 1
236 | }
237 | };
238 | },
239 | event: function (status, id, idFrom) {
240 | if (status.model.nodes[id].type === 'fn') {
241 | status.template.functions[id].events.push({
242 | stream: {
243 | type: "kinesis",
244 | arn: { "Fn::GetAtt": [idFrom, "Arn"] }
245 | }
246 | });
247 | } else {
248 | status.template.resources.Resources[id].Properties.Events['Stream' + idFrom] = {
249 | Type: "Kinesis",
250 | Properties: {
251 | Stream: { "Fn::GetAtt": [idFrom, "Arn"] },
252 | StartingPosition: "TRIM_HORIZON",
253 | BatchSize: 10
254 | }
255 | };
256 | }
257 | },
258 | policy: function (status, id, idTo) {
259 | return {
260 | Effect: "Allow",
261 | Action: ["kinesis:PutRecord", "kinesis:PutRecords"],
262 | Resource: { "Fn::GetAtt": [idTo, "Arn"] }
263 | };
264 | },
265 | },
266 | deliveryStream: {
267 | resource: function (status, node) {
268 | var targetBucketId = null;
269 | var targetFnId = null;
270 | node.to.forEach(function (idTo) { // Target resources
271 | var node_to = status.model.nodes[idTo];
272 | if (node_to.type === 'bucket') {
273 | targetBucketId = idTo;
274 | } else if (node_to.type === 'fn') {
275 | targetFnId = idTo;
276 | }
277 | });
278 | if (targetBucketId == null) {
279 | console.error("Delivery Stream without a destination");
280 | return;
281 | }
282 | var deliveryPolicyId = node.id + "DeliveryPolicy";
283 | var deliveryRoleId = node.id + "DeliveryRole";
284 | // Create the Delivery Strem
285 | status.template.resources.Resources[node.id] = {
286 | DependsOn: [deliveryPolicyId],
287 | Type: 'AWS::KinesisFirehose::DeliveryStream',
288 | Properties: {
289 | ExtendedS3DestinationConfiguration: {
290 | BucketARN: { "Fn::GetAtt": [targetBucketId, "Arn"] },
291 | BufferingHints: {
292 | IntervalInSeconds: 60,
293 | SizeInMBs: 50
294 | },
295 | CompressionFormat: "UNCOMPRESSED",
296 | Prefix: "firehose/",
297 | RoleARN: { "Fn::GetAtt": [deliveryRoleId, "Arn"] }
298 | }
299 | }
300 | };
301 | if (targetFnId !== null) {
302 | status.template.resources.Resources[node.id].Properties
303 | .ExtendedS3DestinationConfiguration.ProcessingConfiguration = {
304 | Enabled: true,
305 | Processors: [{
306 | Parameters: [{
307 | ParameterName: "LambdaArn",
308 | ParameterValue: { Ref: `${targetFnId}LambdaFunction` },
309 | }],
310 | Type: "Lambda"
311 | }]
312 | };
313 | }
314 | // Create a delivery role
315 | status.template.resources.Resources[deliveryRoleId] = {
316 | Type: 'AWS::IAM::Role',
317 | Properties: {
318 | AssumeRolePolicyDocument: {
319 | Version: '2012-10-17',
320 | Statement: [{
321 | Effect: "Allow",
322 | Principal: { Service: "firehose.amazonaws.com" },
323 | Action: 'sts:AssumeRole',
324 | Condition: {
325 | StringEquals: {
326 | 'sts:ExternalId': { Ref: "AWS::AccountId" }
327 | }
328 | }
329 | }]
330 | }
331 | }
332 | };
333 | // Create a delivery policy for the role
334 | status.template.resources.Resources[deliveryPolicyId] = {
335 | Type: 'AWS::IAM::Policy',
336 | Properties: {
337 | PolicyName: "firehose_delivery_policy",
338 | PolicyDocument: {
339 | Version: '2012-10-17',
340 | Statement: [{
341 | Effect: 'Allow',
342 | Action: [
343 | 's3:AbortMultipartUpload',
344 | 's3:GetBucketLocation',
345 | 's3:GetObject',
346 | 's3:ListBucket',
347 | 's3:ListBucketMultipartUploads',
348 | 's3:PutObject'
349 | ],
350 | Resource: {
351 | "Fn::GetAtt": [targetBucketId, "Arn"]
352 | }
353 | }]
354 | },
355 | Roles: [{ Ref: deliveryRoleId }]
356 | }
357 | };
358 | },
359 | event: function () { }, // TODO
360 | policy: function (status, id, idTo) {
361 | return {
362 | Effect: "Allow",
363 | Action: [
364 | "firehose:PutRecord",
365 | "firehose:PutRecordBatch"
366 | ],
367 | // Kinesis Firehose ARN syntax (can't use GetAtt)
368 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
369 | Resource: {
370 | "Fn::Join": [
371 | "",
372 | [
373 | "arn:aws:firehose:",
374 | { Ref: "AWS::Region" },
375 | ":",
376 | { Ref: "AWS::AccountId" },
377 | `:deliverystream/${idTo}`
378 | ]
379 | ]
380 | }
381 | };
382 | },
383 | },
384 | analyticsStream: {
385 | resource: function (status, node) {
386 |
387 | // Input resources
388 | var inputStreamId = null;
389 | var inputDeliveryStreamId = null;
390 | node.from.forEach(function (idFrom) {
391 | var node_from = status.model.nodes[idFrom];
392 | if (node_from.type === 'stream') {
393 | inputStreamId = idFrom;
394 | } else if (node_from.type === 'deliveryStream') {
395 | inputDeliveryStreamId = idFrom;
396 | }
397 | });
398 |
399 | // Output resource
400 | var outputStreamId = null;
401 | var outputDeliveryStreamId = null;
402 | node.to.forEach(function (idTo) {
403 | var node_to = status.model.nodes[idTo];
404 | if (node_to.type === 'stream') {
405 | outputStreamId = idTo;
406 | } else if (node_to.type === 'deliveryStream') {
407 | outputDeliveryStreamId = idTo;
408 | }
409 | });
410 |
411 | var analyticsStreamRoleId = node.id + "Role";
412 | var analyticsStreamOutputId = node.id + "Outputs";
413 |
414 | status.template.resources.Resources[node.id] = {
415 | Type: "AWS::KinesisAnalytics::Application",
416 | Properties: {
417 | ApplicationName: node.id,
418 | Inputs: [{
419 | NamePrefix: "exampleNamePrefix",
420 | InputSchema: {
421 | RecordColumns: [{
422 | Name: "example",
423 | SqlType: "VARCHAR(16)",
424 | Mapping: "$.example"
425 | }],
426 | RecordFormat: {
427 | RecordFormatType: "JSON",
428 | MappingParameters: {
429 | JSONMappingParameters: {
430 | RecordRowPath: "$"
431 | }
432 | }
433 | }
434 | }
435 | }]
436 | }
437 | };
438 |
439 | if (node.description !== '') {
440 | status.template.resources.Resources[node.id]
441 | .Properties.ApplicationDescription = node.description;
442 | }
443 |
444 | if (inputStreamId !== null) {
445 | status.template.resources.Resources[node.id]
446 | .Properties.Inputs[0].KinesisStreamsInput = {
447 | ResourceARN: { "Fn::GetAtt": [inputStreamId, "Arn"] },
448 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] }
449 | };
450 | }
451 |
452 | if (inputDeliveryStreamId !== null) {
453 | status.template.resources.Resources[node.id]
454 | .Properties.Inputs[0].KinesisFirehoseInput = {
455 | // Kinesis Firehose ARN syntax (can't use GetAtt)
456 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
457 | ResourceARN: {
458 | "Fn::Join": [
459 | "",
460 | [
461 | "arn:aws:firehose:",
462 | { Ref: "AWS::Region" },
463 | ":",
464 | { Ref: "AWS::AccountId" },
465 | `:deliverystream/${inputDeliveryStreamId}`
466 | ]
467 | ]
468 | },
469 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] }
470 | };
471 | }
472 |
473 | status.template.resources.Resources[analyticsStreamRoleId] = {
474 | Type: "AWS::IAM::Role",
475 | Properties: {
476 | AssumeRolePolicyDocument: {
477 | Version: "2012-10-17",
478 | Statement: [{
479 | Effect: "Allow",
480 | Principal: {
481 | Service: "kinesisanalytics.amazonaws.com"
482 | },
483 | Action: "sts:AssumeRole"
484 | }]
485 | },
486 | Path: "/",
487 | Policies: [{
488 | PolicyName: "Open",
489 | PolicyDocument: {
490 | Version: "2012-10-17",
491 | Statement: [{
492 | Effect: "Allow",
493 | Action: "*",
494 | Resource: "*"
495 | }]
496 | }
497 | }]
498 | }
499 | };
500 |
501 | status.template.resources.Resources[analyticsStreamOutputId] = {
502 | Type: "AWS::KinesisAnalytics::ApplicationOutput",
503 | DependsOn: node.id,
504 | Properties: {
505 | ApplicationName: { Ref: node.id },
506 | Output: {
507 | Name: "exampleOutput",
508 | DestinationSchema: {
509 | RecordFormatType: "CSV"
510 | }
511 | }
512 | }
513 | };
514 |
515 | if (outputStreamId !== null) {
516 | status.template.resources.Resources[analyticsStreamOutputId]
517 | .Properties.Output.KinesisStreamsOutput = {
518 | ResourceARN: { "Fn::GetAtt": [outputStreamId, "Arn"] },
519 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] }
520 | };
521 | }
522 |
523 | if (outputDeliveryStreamId !== null) {
524 | status.template.resources.Resources[analyticsStreamOutputId]
525 | .Properties.Output.KinesisFirehoseOutput = {
526 | // Kinesis Firehose ARN syntax (can't use GetAtt)
527 | // arn:aws:firehose:region:account-id:deliverystream/delivery-stream-name
528 | ResourceARN: {
529 | "Fn::Join": [
530 | "",
531 | [
532 | "arn:aws:firehose:",
533 | { Ref: "AWS::Region" },
534 | ":",
535 | { Ref: "AWS::AccountId" },
536 | `:deliverystream/${outputDeliveryStreamId}`
537 | ]
538 | ]
539 | },
540 | RoleARN: { "Fn::GetAtt": [analyticsStreamRoleId, "Arn"] }
541 | };
542 | }
543 |
544 | },
545 | event: function () { }, // TODO
546 | policy: function () { } // TODO
547 | },
548 | schedule: {
549 | resource: function (status, node) {
550 | // Nothing to do
551 | },
552 | event: function (status, id, idFrom) {
553 | if (status.model.nodes[id].type === 'fn') {
554 | status.template.functions[id].events.push({
555 | schedule: "rate(5 minutes)"
556 | });
557 | } else {
558 | status.template.resources.Resources[id].Properties.Events['Schedule' + idFrom] = {
559 | Type: "Schedule",
560 | Properties: {
561 | Schedule: "rate(5 minutes)"
562 | }
563 | };
564 | }
565 | },
566 | policy: function () { } // This has no sense
567 | },
568 | topic: {
569 | resource: function (status, node) {
570 | status.template.resources.Resources[node.id] = {
571 | Type: "AWS::SNS::Topic"
572 | };
573 | },
574 | event: function (status, id, idFrom) {
575 | if (status.model.nodes[id].type === 'fn') {
576 | status.template.functions[id].events.push({
577 | sns: idFrom
578 | });
579 | } else {
580 | status.template.resources.Resources[id].Properties.Events['Topic' + idFrom] = {
581 | Type: "SNS",
582 | Properties: {
583 | Topic: { Ref: idFrom }
584 | }
585 | };
586 | }
587 | },
588 | policy: function (status, id, idTo) {
589 | return {
590 | Effect: "Allow",
591 | Action: "sns:Publish",
592 | Resource: { Ref: idTo } // For an SNS topic, it returns the ARN
593 | };
594 | },
595 | },
596 | fn: {
597 | resource: function (status, node) {
598 | // Check for and build a .gitignore if we haven't already
599 | if (!status.files[".gitignore"]) {
600 | status.files[".gitignore"] = runtimes[status.runtime].gitignore;
601 | }
602 | status.template.functions[node.id] = {
603 | handler: node.id + "." + runtimes[status.runtime].handler
604 | };
605 | if (node.description !== '') {
606 | status.template.functions[node.id].description = node.description;
607 | }
608 | status.files[node.id + '.' + runtimes[status.runtime].fileExtension] =
609 | runtimes[status.runtime].startingCode;
610 |
611 | if (node.from.length > 0) { // There are triggers for this function
612 | status.template.functions[node.id].events = [];
613 | node.from.forEach(function (idFrom) {
614 | console.log("Trigger " + idFrom + " -> " + node.id);
615 | renderingRules[status.model.nodes[idFrom].type].event(status, node.id, idFrom);
616 | });
617 | }
618 | if (node.to.length > 0) { // There are resources target of this function
619 | var policy = {
620 | Version: "2012-10-17",
621 | Statement: []
622 | };
623 | node.to.forEach(function (idTo) {
624 | console.log("Policy " + node.id + " -> " + idTo);
625 | policy.Statement.push(
626 | renderingRules[status.model.nodes[idTo].type]
627 | .policy(status, node.id, idTo)
628 | );
629 | });
630 |
631 | if (status.model.nodes[node.id].type !== 'fn') {
632 | status.template.resources.Resources[node.id].Properties.Policies.push(policy);
633 | }
634 | }
635 | },
636 | event: function () { }, // Nothing to do, this is not a trigger, but a fn to fn invocation
637 | policy: function (status, id, idTo) {
638 | return {
639 | Effect: "Allow",
640 | Action: ["lambda:Invoke", "lambda:InvokeAsync"],
641 | Resource: { "Fn::GetAtt": [idTo, "Arn"] }
642 | };
643 | }
644 | },
645 | stepFn: {
646 | resource: function (status, node) {
647 | status.template.resources.Resources[node.id] = {
648 | Type: "AWS::StepFunctions::StateMachine",
649 | Properties: {
650 | // The DefinitionString is added later
651 | // This role is automatically created by the AWS console
652 | // the first time you create a state machine in a region
653 | RoleArn: {
654 | "Fn::Join": [
655 | "",
656 | [
657 | "arn:aws:iam::",
658 | { Ref: "AWS::AccountId" },
659 | ":role/service-role/StatesExecutionRole-",
660 | { Ref: "AWS::Region" }
661 | ]
662 | ]
663 | },
664 | },
665 | };
666 | var definitionString = {
667 | Comment: "A Hello World example",
668 | StartAt: "HelloWorld",
669 | States: {
670 | HelloWorld: {
671 | Type: "Pass",
672 | Result: "Hello World!",
673 | End: true
674 | }
675 | }
676 | };
677 | // The DefinitionString must be a string with JSON syntax within the template
678 | status.template.resources.Resources[node.id].Properties.DefinitionString =
679 | JSON.stringify(definitionString, null, 2);
680 | },
681 | event: function () { }, // Nothing to do
682 | policy: function (status, id, idTo) {
683 | return {
684 | "Effect": "Allow",
685 | "Action": [
686 | "states:DescribeExecution",
687 | "states:GetExecutionHistory",
688 | "states:ListExecutions",
689 | "states:StartExecution",
690 | "states:StopExecution"
691 | ],
692 | "Resource": [
693 | { Ref: idTo }
694 | ]
695 | };
696 | }
697 | },
698 | cognitoIdentity: {
699 | resource: function (status, node) {
700 | var cognitoUnauthRoleId = node.id + "CognitoUnauthRole";
701 | var cognitoUnauthPolicyId = node.id + "CognitoUnauthPolicy";
702 | status.template.resources.Resources[node.id] = {
703 | Type: "AWS::Cognito::IdentityPool",
704 | Properties: {
705 | AllowUnauthenticatedIdentities: true // TODO Maybe this is not a secure default ???
706 | }
707 | };
708 | status.template.resources.Resources[cognitoUnauthRoleId] = {
709 | Type: 'AWS::IAM::Role',
710 | Properties: {
711 | AssumeRolePolicyDocument: {
712 | Version: '2012-10-17',
713 | Statement: [{
714 | Effect: "Allow",
715 | Principal: { Federated: "cognito-identity.amazonaws.com" },
716 | Action: 'sts:AssumeRoleWithWebIdentity',
717 | Condition: {
718 | StringEquals: {
719 | "cognito-identity.amazonaws.com:aud": { Ref: node.id }
720 | },
721 | "ForAnyValue:StringLike": {
722 | "cognito-identity.amazonaws.com:amr": "unauthenticated"
723 | }
724 | }
725 | }]
726 | }
727 | }
728 | };
729 | // Create a delivery policy for the role
730 | status.template.resources.Resources[cognitoUnauthPolicyId] = {
731 | Type: 'AWS::IAM::Policy',
732 | Properties: {
733 | PolicyName: "cognito_unauth_policy",
734 | PolicyDocument: {
735 | Version: '2012-10-17',
736 | Statement: []
737 | },
738 | Roles: [ { Ref: cognitoUnauthRoleId } ]
739 | }
740 | };
741 | // Output resources
742 | node.to.forEach(function (idTo) {
743 | // not used??? var node_to = status.model.nodes[idTo];
744 | status.template.resources.Resources[cognitoUnauthPolicyId]
745 | .Properties.PolicyDocument.Statement.push(
746 | renderingRules[status.model.nodes[idTo].type]
747 | .policy(status, node.id, idTo)
748 | );
749 | });
750 | },
751 | event: function () { }, // TODO ???
752 | policy: function () { } // TODO ???
753 | },
754 | iotRule: {
755 | resource: function (status, node) {
756 | status.template.resources.Resources[node.id] = {
757 | Type: "AWS::IoT::TopicRule",
758 | Properties: {
759 | TopicRulePayload: {
760 | RuleDisabled: "true", // safe choice
761 | Sql: "Select temp FROM 'Some/Topic' WHERE temp > 60",
762 | Actions: []
763 | }
764 | }
765 | };
766 | if (node.description !== '') {
767 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Description = node.description;
768 | }
769 | // Output resources
770 | node.to.forEach(function (idTo) {
771 | var node_to = status.model.nodes[idTo];
772 | switch (node_to.type) {
773 | case 'fn':
774 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Actions.push({
775 | Lambda: {
776 | FunctionArn: `${idTo}LambdaFunction`
777 | }
778 | });
779 | break;
780 | case 'iotRule': // republish
781 | var republishRoleId = idTo + "PublishRole";
782 | status.template.resources.Resources[node.id].Properties.TopicRulePayload.Actions.push({
783 | Republish: {
784 | Topic: "Output/Topic",
785 | RoleArn: { "Fn::GetAtt": [republishRoleId, "Arn"] }
786 | }
787 | });
788 | status.template.resources.Resources[republishRoleId] = {
789 | Type: "AWS::IAM::Role",
790 | Properties: {
791 | AssumeRolePolicyDocument: {
792 | Version: "2012-10-17",
793 | Statement: [{
794 | Effect: "Allow",
795 | Action: [ "sts:AssumeRole" ],
796 | Principal: {
797 | Service: [ "iot.amazonaws.com" ]
798 | }
799 | }]
800 | },
801 | Policies: [{
802 | PolicyName: "publish",
803 | PolicyDocument: {
804 | Version: "2012-10-17",
805 | Statement: [{
806 | Effect: "Allow",
807 | Action: "iot:Publish",
808 | Resource: {
809 | "Fn::Join": [
810 | "",
811 | [
812 | "arn:aws:iot:",
813 | { Ref: "AWS::Region" },
814 | ":",
815 | { Ref: "AWS::AccountId" },
816 | ":topic/Outpu/*"
817 | ]
818 | ]
819 | }
820 | }]
821 | }
822 | }]
823 | }
824 | };
825 | break;
826 | default:
827 | throw "Error: connection type not supported (" + node_to.type + ")";
828 | }
829 | });
830 | },
831 | event: function () { },
832 | policy: function () { }
833 | }
834 | };
835 |
836 | function render(model, runtime, deployment) {
837 | console.log('Using Serverless Framework...');
838 | var files = {};
839 | var template = {
840 | service: "serverless",
841 | provider: {
842 | name: "aws",
843 | runtime: runtime
844 | },
845 | functions: { },
846 | resources: {
847 | Resources: { },
848 | Outputs: { }
849 | }
850 | };
851 |
852 | var status = {
853 | model: model,
854 | runtime: runtime,
855 | files: files,
856 | template: template
857 | };
858 |
859 | for (var id in model.nodes) {
860 | var node = model.nodes[id];
861 | renderingRules[node.type].resource(status, node);
862 | }
863 |
864 | console.log(template); // Still in JSON
865 | console.log(JSON.stringify(template, null, 4)); // JSON -> text
866 |
867 | for (var r in template.Resources) {
868 | console.log(r + " -> YAML");
869 | console.log(jsyaml.safeDump(template.Resources[r], { lineWidth: 1024 }));
870 | }
871 |
872 | // Line breaks can introduce YAML syntax (e.g. >-) that will put some variables
873 | // (e.g. AWS::Region) between quotes.
874 | // Single quotes must be removed for functions (e.g. Fn::GetAtt) to work.
875 | files['serverless.yml'] = jsyaml.safeDump(template, { lineWidth: 1024 }).replace(/'(!.+)'/g, "$1");
876 |
877 | return files;
878 | }
879 |
880 | module.exports = render;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import $ from 'jquery';
4 | //window.jQuery = $;
5 | //window.$ = $;
6 |
7 | import 'bootstrap';
8 | import 'bootstrap/dist/css/bootstrap.min.css';
9 |
10 | import { Network } from 'vis/index-network';
11 | import { DataSet } from 'vis/index-timeline-graph2d';
12 | import 'vis/dist/vis-network.min.css';
13 |
14 | var JSZip = require("jszip");
15 | import { saveAs } from 'file-saver';
16 |
17 | const sam = require('./engines/sam');
18 | const servfrmwk = require('./engines/servfrmwk');
19 |
20 | // Engines to build the application in different formats (AWS SAM, ...)
21 | const engines = {
22 | sam: sam,
23 | servfrmwk: servfrmwk
24 | };
25 |
26 | const enginesTips = {
27 | sam: `You can now build this application using the AWS SAM CLI:
28 |
29 | sam package --s3-bucket --output-template-file packaged.json
30 |
31 | sam deploy --template-file packaged.json --stack-name --capabilities CAPABILITY_IAM
32 | `,
33 | servfrmwk: `You can now build this application using the Serverless Framework:
34 |
35 | serverless deploy`
36 | };
37 |
38 | const deploymentPreferenceTypes = {
39 | '': 'None',
40 | Canary10Percent5Minutes: 'Canary 10% for 5\'',
41 | Canary10Percent10Minutes: 'Canary 10% for 10\'',
42 | Canary10Percent15Minutes: 'Canary 10% for 15\'',
43 | Canary10Percent30Minutes: 'Canary 10% for 30\'',
44 | Linear10PercentEvery1Minute: 'Linear 10% every 1\'',
45 | Linear10PercentEvery2Minutes: 'Linear 10% every 2\'',
46 | Linear10PercentEvery3Minutes: 'Linear 10% every 3\'',
47 | Linear10PercentEvery10Minutes: 'Linear 10% every 10\'',
48 | AllAtOnce: 'All at Once'
49 | };
50 |
51 | const nodeTypes = {
52 | api: {
53 | name: 'API Gateway',
54 | image: './img/aws/Amazon-API-Gateway.png'
55 | },
56 | cognitoIdentity: {
57 | name: 'Cognito Identity',
58 | image: './img/aws/Amazon-Cognito.png'
59 | },
60 | table: {
61 | name: 'DynamoDB Table',
62 | image: './img/aws/Amazon-DynamoDB_Table.png'
63 | },
64 | analyticsStream: {
65 | name: 'Kinesis Data Analytics',
66 | image: './img/aws/Amazon-Kinesis-Data-Analytics.png'
67 | },
68 | deliveryStream: {
69 | name: 'Kinesis Data Firehose',
70 | image: './img/aws/Amazon-Kinesis-Data-Firehose.png'
71 | },
72 | stream: {
73 | name: 'Kinesis Data Stream',
74 | image: './img/aws/Amazon-Kinesis-Data-Streams.png'
75 | },
76 | iotRule: {
77 | name: 'IoT Topic Rule',
78 | image: './img/aws/IoT_Rule.png'
79 | },
80 | fn: {
81 | name: 'Lambda Function',
82 | image: './img/aws/AWS-Lambda_Function.png'
83 | },
84 | bucket: {
85 | name: 'S3 Bucket',
86 | image: './img/aws/Amazon-S3_Bucket.png'
87 | },
88 | schedule: {
89 | name: 'Schedule',
90 | image: './img/aws/Amazon-CloudWatch_Event-Time-Based.png'
91 | },
92 | topic: {
93 | name: 'SNS Topic',
94 | image: './img/aws/Amazon-SNS_Topic.png'
95 | },
96 | stepFn: {
97 | name: 'Step Function',
98 | image: './img/aws/AWS-Step-Functions.png'
99 | },
100 | };
101 |
102 | const nodeConnections = {
103 | bucket: {
104 | topic: { action: 'notification' },
105 | fn: { action: 'trigger' }
106 | },
107 | table: {
108 | fn: { action: 'stream' }
109 | },
110 | api: {
111 | fn: { action: 'integration' },
112 | stepFn: { action: 'integration' }
113 | },
114 | stream: {
115 | fn: { action: 'trigger' },
116 | analyticsStream: { action: 'input' },
117 | deliveryStream: { action: 'deliver' }
118 | },
119 | deliveryStream: {
120 | bucket: { action: 'destination' },
121 | fn: { action: 'transform' } // To transform data in the stream
122 | },
123 | analyticsStream: {
124 | stream: { action: 'output' },
125 | deliveryStream: { action: 'output' }
126 | },
127 | schedule: {
128 | deliveryStream: { action: 'target' },
129 | stream: { action: 'target' },
130 | topic: { action: 'target' },
131 | fn: { action: 'target' }
132 | },
133 | topic: {
134 | fn: { action: 'trigger' }
135 | },
136 | fn: {
137 | bucket: { action: 'read/write' },
138 | table: { action: 'read/write' },
139 | api: { action: 'invoke' },
140 | stream: { action: 'put' },
141 | deliveryStream: { action: 'put' },
142 | topic: { action: 'notification' },
143 | fn: { action: 'invoke' },
144 | stepFn: { action: 'activity' }
145 | },
146 | stepFn: {
147 | fn: { action: 'invoke' },
148 | },
149 | cognitoIdentity: {
150 | fn: { action: 'authorize' },
151 | api: { action: 'authorize' }
152 | },
153 | iotRule: {
154 | fn: { action: 'invoke' },
155 | iotRule: { action: 'republish' }
156 | // These connections require an external/service role
157 | // stream: { action: 'put' },
158 | // deliveryStream: { action: 'put' },
159 | // table: { action: 'write' },
160 | // topic: { action: 'notification' },
161 | }
162 | };
163 |
164 | function getUrlParams() {
165 | let p = {};
166 | let match,
167 | pl = /\+/g, // Regex for replacing addition symbol with a space
168 | search = /([^&=]+)=?([^&]*)/g,
169 | decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
170 | query = window.location.search.substring(1);
171 | while (match = search.exec(query))
172 | p[decode(match[1])] = decode(match[2]);
173 | return p;
174 | }
175 |
176 | function setSelectOptions(id, options, message) {
177 | let $el = $("#" + id);
178 | $el.empty(); // remove old options
179 | $el.append($(" ", { 'disabled': "disabled", 'selected': "selected", 'value': "" })
180 | .text(message));
181 | $.each(options, function (key, value) {
182 | let description;
183 | if (value.hasOwnProperty('name')) {
184 | description = value.name;
185 | } else {
186 | description = value;
187 | }
188 | $el.append($(" ", { 'value': key })
189 | .text(description));
190 | });
191 | }
192 |
193 | /*
194 | function setRadioOptions(id, options) {
195 | let $el = $("#" + id);
196 | $el.empty(); // remove old options
197 | $.each(options, function (key, value) {
198 | $el.append(
199 | $("
", { 'class': 'radio' }).append(
200 | $(" ", { 'type': 'radio', 'name': id, 'value': key, 'id': key })
201 | ).append(
202 | $(" ", { 'class': 'radio', 'for': key }).text(value)
203 | )
204 | );
205 | });
206 | }
207 | */
208 |
209 | function networkHeight() {
210 | const h = $(window).height() - $("#header").height() - 40;
211 | const w = $(".container-fluid").width() - 20;
212 | $("#networkContainer").height(h);
213 | $("#networkContainer").width(w);
214 | }
215 | $(document).ready(networkHeight);
216 | $(window).resize(networkHeight).resize();
217 |
218 | $("#mainForm").submit(function (event) {
219 | event.preventDefault();
220 | });
221 |
222 | $("#nodeForm").submit(function (event) {
223 | event.preventDefault();
224 | var id = $("#nodeId").val();
225 | var type = $("#nodeTypeSelect :selected").val();
226 | var description = $("#nodeDescription").val();
227 | var label = $("#nodeTypeSelect :selected").text() + '\n' + id;
228 | var nodeData = modalCallback['nodeModal'].data;
229 | var callback = modalCallback['nodeModal'].callback;
230 | if (type === undefined || type == "") {
231 | alert("Please choose a resource type.");
232 | } else if (id === '') {
233 | alert("Please provide a unique ID for the node.");
234 | } else if (!$("#nodeId").prop('disabled') && nodes.get(id) !== null) {
235 | alert("Node ID already in use.");
236 | } else {
237 | nodeData.id = id;
238 | nodeData.label = label;
239 | if (description !== '') {
240 | nodeData.title = description; // For the tooltip
241 | } else if ('title' in nodeData) {
242 | nodeData.title = undefined;
243 | }
244 | nodeData.model = {
245 | type: type,
246 | description: description
247 | };
248 | nodeData.group = type;
249 | nodeData.shadow = false; // quick fix for updates
250 | callback(nodeData);
251 | modalCallback['nodeModal'] = null;
252 | $("#nodeModal").modal('hide');
253 | }
254 | });
255 |
256 | $("#nodeModal").on('hide.bs.modal', function () {
257 | if (modalCallback['nodeModal'] !== null) {
258 | var callback = modalCallback['nodeModal'].callback;
259 | callback(null);
260 | modalCallback['nodeModal'] = null;
261 | }
262 | });
263 |
264 | $("#screenshotButton").click(function () {
265 | var appName = $("#appName").val();
266 | if (appName === '') {
267 | alert('Please provide an Application Name.');
268 | return;
269 | }
270 | var canvas = $("#networkContainer canvas")[0];
271 | canvas.toBlob(function(blob){
272 | saveAs(blob, appName + ".png");
273 | });
274 | });
275 |
276 | $("#exportButton").click(function () {
277 | var appName = $("#appName").val();
278 | if (appName === '') {
279 | alert('Please provide an Application Name.');
280 | return;
281 | }
282 | var jsonData = exportNetwork();
283 | var blob = new Blob([jsonData], { type: "application/json;charset=utf-8" });
284 | saveAs(blob, appName + ".json");
285 | });
286 |
287 | $("#importButton").click(function () {
288 | $("#importData").val('');
289 | $("#importModal").modal();
290 | });
291 |
292 | $("#importForm").submit(function (event) {
293 | event.preventDefault();
294 | var importData = $("#importData").val();
295 | importNetwork(JSON.parse(importData));
296 | $("#importModal").modal('hide');
297 | });
298 |
299 | $("#buildButton").click(function () {
300 | var appName = $("#appName").val();
301 | if (appName === '') {
302 | alert('Please provide an Application Name.');
303 | return;
304 | }
305 | var runtime = $("#runtime :selected").val();
306 | var deployment = $("#deployment :selected").val();
307 | var engine = $("#engine :selected").val();
308 | console.log("Building " + appName + " -> " + runtime + " / " + engine);
309 | var model = {
310 | app: appName,
311 | nodes: {}
312 | };
313 | nodes.forEach(function (n) {
314 | console.log(n.id + ' (' + n.model.type + ')');
315 | model.nodes[n.id] = {
316 | id: n.id,
317 | type: n.model.type,
318 | description: n.model.description,
319 | to: [],
320 | from: []
321 | };
322 | network.getConnectedNodes(n.id, 'to').forEach(function (cid) {
323 | console.log(n.id + ' to ' + cid);
324 | model.nodes[n.id]['to'].push(cid);
325 | });
326 | network.getConnectedNodes(n.id, 'from').forEach(function (cid) {
327 | console.log(n.id + ' from ' + cid);
328 | model.nodes[n.id]['from'].push(cid);
329 | });
330 | });
331 | console.log("Building...");
332 |
333 | var files = engines[engine](model, runtime, deployment);
334 |
335 | var zip = new JSZip();
336 |
337 | for (var f in files) {
338 | console.log("=== " + f + " ===");
339 | console.log(files[f]);
340 | zip.file(f, files[f]);
341 | }
342 |
343 | zip.generateAsync({ type: "blob" })
344 | .then(function (content) {
345 | // see FileSaver.js
346 | saveAs(content, model.app + ".zip");
347 | alert(enginesTips[engine]);
348 | });
349 |
350 | });
351 |
352 | function exportNetwork() {
353 | var exportData = {
354 | nodes: [],
355 | edges: []
356 | };
357 |
358 | nodes.forEach(function (n) {
359 | exportData.nodes.push(n);
360 | });
361 |
362 | edges.forEach(function (n) {
363 | exportData.edges.push(n);
364 | });
365 |
366 | var exportJson = JSON.stringify(exportData);
367 |
368 | return exportJson;
369 | }
370 |
371 | function importNetwork(importData) {
372 |
373 | nodes = new DataSet(importData.nodes);
374 | edges = new DataSet(importData.edges);
375 |
376 | networkData = {
377 | nodes: nodes,
378 | edges: edges
379 | };
380 |
381 | $("#physicsContainer").empty(); // Otherwise another config panel is added
382 |
383 | network = new Network(networkContainer, networkData, networkOptions);
384 | network.redraw();
385 | }
386 |
387 | function getEdgeStyle(node, edgeData) {
388 | switch (node.model.type) {
389 | case 'fn':
390 | edgeData.color = { color: 'green' };
391 | edgeData.dashes = true;
392 | break;
393 | default:
394 | edgeData.color = { color: 'blue' };
395 | }
396 | }
397 |
398 | function networkAddNode(nodeData, callback) {
399 | modalCallback['nodeModal'] = {
400 | data: nodeData,
401 | callback: callback
402 | };
403 | $("#nodeModalTitle").text("Add Node");
404 | $("#nodeId").val('');
405 | $("#nodeDescription").val('');
406 | $("#nodeTypeSelect").val('');
407 | $("#nodeTypeSelect").prop('disabled', false);
408 | $("#nodeId").prop('disabled', false);
409 | $("#nodeId").prop('disabled', false);
410 | $("#nodeModal").modal();
411 | }
412 |
413 | function networkEditNode(nodeData, callback) {
414 | modalCallback['nodeModal'] = {
415 | data: nodeData,
416 | callback: callback
417 | };
418 | $("#nodeModalTitle").text("Edit Node");
419 | console.log(nodeData.model.type);
420 | $("#nodeTypeSelect").val(nodeData.model.type);
421 | $("#nodeTypeSelect").prop('disabled', true);
422 | $("#nodeId").val(nodeData.id);
423 | $("#nodeId").prop('disabled', true);
424 | $("#nodeDescription").val(nodeData.model.description);
425 | $("#nodeModal").modal();
426 | }
427 |
428 | function networkAddEdge(edgeData, callback) {
429 | console.log(edgeData.from + " -> " + edgeData.to);
430 | var nodeFrom = nodes.get(edgeData.from);
431 | var nodeTo = nodes.get(edgeData.to);
432 | if (!(nodeTo.model.type in nodeConnections[nodeFrom.model.type])) {
433 | var toTypeList = Object.keys(nodeConnections[nodeFrom.model.type])
434 | .map(function (t) { return nodeTypes[t].name });
435 | var fromTypeList = Object.keys(nodeConnections)
436 | .filter(function (t) { return nodeTo.model.type in nodeConnections[t] })
437 | .map(function (t) { return nodeTypes[t].name });
438 | var msg = "You can't connect " + nodeTypes[nodeFrom.model.type].name +
439 | " to " + nodeTypes[nodeTo.model.type].name + ".\n" +
440 | "You can connect " + nodeTypes[nodeFrom.model.type].name +
441 | " to " + toTypeList.join(', ') + ".\n" +
442 | "You can connect " + fromTypeList.join(', ') +
443 | " to " + nodeTypes[nodeTo.model.type].name + ".";
444 | alert(msg);
445 | } else {
446 | edgeData.label = nodeConnections[nodeFrom.model.type][nodeTo.model.type].action;
447 | getEdgeStyle(nodeFrom, edgeData);
448 | console.log(edgeData);
449 | if (edgeData.from === edgeData.to) {
450 | var r = confirm("Do you want to connect the node to itself?");
451 | if (r === true) {
452 | callback(edgeData);
453 | }
454 | } else {
455 | callback(edgeData);
456 | }
457 | }
458 | }
459 |
460 | // create an array with nodes
461 | var nodes = new DataSet();
462 |
463 | // create an array with edges
464 | var edges = new DataSet();
465 |
466 | var networkData = {
467 | nodes: nodes,
468 | edges: edges
469 | };
470 |
471 | // create a network
472 | var networkOptions = {
473 | manipulation: {
474 | enabled: true,
475 | addNode: networkAddNode,
476 | editNode: networkEditNode,
477 | addEdge: networkAddEdge,
478 | editEdge: false // Better to delete and add again
479 | },
480 | nodes: {
481 | font: {
482 | size: 14,
483 | strokeWidth: 2
484 | }
485 | },
486 | edges: {
487 | arrows: 'to',
488 | color: {
489 | color: 'red',
490 | highlight: 'red'
491 | },
492 | font: {
493 | size: 12,
494 | align: 'middle',
495 | strokeWidth: 2
496 | }
497 | },
498 | groups: {},
499 | physics: {
500 | enabled: true,
501 | barnesHut: {
502 | avoidOverlap: 0.1
503 | },
504 | forceAtlas2Based: {
505 | avoidOverlap: 0.1
506 | },
507 | },
508 | configure: {
509 | enabled: true,
510 | container: $("#physicsContainer")[0],
511 | filter: 'physics',
512 | showButton: false
513 | }
514 | };
515 |
516 | // Filling the groups
517 | for (let type in nodeTypes) {
518 | let color;
519 | switch (type) {
520 | case 'fn':
521 | color = 'green';
522 | break;
523 | default:
524 | color = 'blue';
525 | }
526 | networkOptions.groups[type] = {
527 | shape: 'image',
528 | image: nodeTypes[type].image,
529 | mass: 1.2,
530 | shapeProperties: {
531 | useBorderWithImage: true
532 | },
533 | color: {
534 | border: 'white',
535 | background: 'white',
536 | highlight: {
537 | border: color,
538 | background: 'white'
539 | }
540 | }
541 | };
542 | }
543 |
544 | var networkContainer = document.getElementById('networkContainer');
545 |
546 | var network = new Network(networkContainer, networkData, networkOptions);
547 |
548 | // To manage callbacks from modal dialogs
549 | var modalCallback = {};
550 |
551 | function init() {
552 | setSelectOptions('nodeTypeSelect', nodeTypes, "Please choose");
553 | setSelectOptions('deployment', deploymentPreferenceTypes, "Deployment Preference");
554 | var urlParams = getUrlParams();
555 | var importLink = urlParams['import'] || null;
556 | if (importLink !== null) {
557 | $.get(importLink, function(result){
558 | importNetwork(result);
559 | });
560 | }
561 | }
562 |
563 | window.onload = function () { init() };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require("webpack");
3 |
4 | module.exports = {
5 | entry: './src/index.js',
6 | output: {
7 | filename: 'main.js',
8 | path: path.resolve(__dirname, 'dist')
9 | },
10 | module: {
11 | rules: [{
12 | test: /\.css$/,
13 | use: ['style-loader', 'css-loader']
14 | }, {
15 | test: /\.png$/i,
16 | loaders: [
17 | {
18 | loader: 'file-loader',
19 | options: {
20 | outputPath: 'img/vis',
21 | }
22 | },
23 | {
24 | loader: 'image-webpack-loader',
25 | query: {
26 | progressive: true,
27 | pngquant: {
28 | quality: '65-90',
29 | speed: 4
30 | }
31 | }
32 | }
33 | ]
34 |
35 | }]
36 | },
37 | plugins: [
38 | new webpack.ProvidePlugin({
39 | $: "jquery",
40 | jQuery: "jquery"
41 | })
42 | ]
43 | };
44 |
--------------------------------------------------------------------------------