Ok, here is a sample of the output from my flow that "zips" up the flow file into a directory structure, suitable for committing to a source code repo...
On the left you can see the individual "tab" folders, created to match the "tabs" across the top of the flow editor. Inside are .json files for each node -- and if any multi-line text fields are found (e.g. function
node text), it is placed into a file named after the pattern <field>.<type>
(e.g. the func.js
file shown).
There are some unfortunate side-effects of trying to decompose a random JSON structure into a directory tree -- in order to ensure unique filenames, I chose to include the node.id
value in the folder and file names. But I also tried to show the node name where it made sense, to make it easier to find the node you are looking to edit inside an IDE.
I really like the idea of using the "Syntax Highlighting" value to set the file extension... I guess that would be a simple enhancement. My intention was to have another flow reassemble the flows.json
file from the directory structure, but I never got that far. Here is the flow that I have so far -- N.B. it does require a contrib node for creating the zip file:
flow source here
[
{
"id": "2439e87451b07802",
"type": "comment",
"z": "5ec1131187731246",
"name": "Extract the runtime flows.json file\\n into a zipped directory tree",
"info": "",
"x": 210,
"y": 500,
"wires": []
},
{
"id": "25085272ac58faf4",
"type": "http in",
"z": "5ec1131187731246",
"name": "",
"url": "/zip/flows",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 190,
"y": 560,
"wires": [
[
"6c9f89f19c8565ca"
]
]
},
{
"id": "32ce4581186384c1",
"type": "inject",
"z": "5ec1131187731246",
"name": "test zip flows",
"props": [
{
"p": "topic",
"v": "$now('[Y][M02][D02]')",
"vt": "jsonata"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 210,
"y": 600,
"wires": [
[
"6c9f89f19c8565ca"
]
]
},
{
"id": "ec8b17d3cf1caf66",
"type": "debug",
"z": "5ec1131187731246",
"name": "flows out",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 560,
"wires": []
},
{
"id": "6c9f89f19c8565ca",
"type": "http request",
"z": "5ec1131187731246",
"name": "GET /admin/flows",
"method": "GET",
"ret": "obj",
"url": "http://localhost:1880/admin/flows",
"tls": "",
"x": 450,
"y": 560,
"wires": [
[
"ec8b17d3cf1caf66",
"0e534a79b3b89185"
]
]
},
{
"id": "92889fe06ee5d09a",
"type": "switch",
"z": "5ec1131187731246",
"name": "msg.res?",
"property": "res",
"propertyType": "msg",
"rules": [
{
"t": "istype",
"v": "object",
"vt": "object"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 780,
"y": 700,
"wires": [
[
"d5f8028683a4a7d3"
],
[
"248320dfd65d34f6"
]
]
},
{
"id": "b39272e50866b262",
"type": "http response",
"z": "5ec1131187731246",
"name": "",
"statusCode": "",
"headers": {},
"x": 930,
"y": 840,
"wires": []
},
{
"id": "487ca486.c231ac",
"type": "zip",
"z": "5ec1131187731246",
"name": "create zip file",
"mode": "compress",
"filename": "flows_shr.zip",
"compressionlevel": "0",
"outasstring": false,
"x": 570,
"y": 640,
"wires": [
[
"92889fe06ee5d09a",
"38824e8c756d1991"
]
]
},
{
"id": "0e534a79b3b89185",
"type": "change",
"z": "5ec1131187731246",
"name": "filename + filedata",
"rules": [
{
"t": "set",
"p": "filename",
"pt": "msg",
"to": "'~/backups/flow_dev.' & $now('[Y][M02][D02]') & '.zip'",
"tot": "jsonata"
},
{
"t": "set",
"p": "headers.content-disposition",
"pt": "msg",
"to": "'attachment; filename=\"' & (filename ? filename : \"flows.zip\") & '\"'",
"tot": "jsonata"
},
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "(\t $tabnodes := payload[type='tab'];\t $nodegrps := payload[type='group'].nodes@$n {\t $n: {\t \"id\": $.id,\t \"name\": $.name\t }\t };\t\t payload#$i[].(\t $element := $;\t $tabnode := (type = 'tab' ? $ : $tabnodes[id = $element.z]);\t $grpnode := $lookup($nodegrps, $element.id);\t $elename := $element.[label, name, summary, id][0].$replace(/\\\\n.*/, '');\t $tabname := z ? 'tab.' & ($tabnode.label ? $tabnode.label.$replace(' ', '_').$replace(/[^\\w_]+/, '-') : z);\t $grpname := $grpnode ? 'group.' & ($grpnode.name ? $grpnode.name.$replace(' ', '_').$replace(/[^\\w_]+/, '-') : $grpnode.id);\t $typname := type.$replace(' ', '_') & '.' & ($elename ? $elename.$replace(' ', '_').$replace(/[^\\w_]+/, '-') : id);\t $filepath := [$tabname, $grpname, $typname]~>$join('/');\t\t [\t /* output the entire node's json */\t {\t \"filename\": $filepath & '.json',\t \"payload\": $string($element, true)\t },\t /* handle multi-line text content */\t $spread($element)[$string($.*).$contains(/\\n/)].(\t $fieldname := $keys($)[0];\t $fieldext := $fieldname = 'func' ? 'js' : 'txt';\t {\t \"filename\": $filepath & '/' & $fieldname & '.' & $fieldext,\t \"payload\": $.*\t }\t )\t ]\t )\t)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 510,
"y": 600,
"wires": [
[
"487ca486.c231ac",
"3641660dd8894cd5"
]
]
},
{
"id": "38824e8c756d1991",
"type": "debug",
"z": "5ec1131187731246",
"name": "zip output",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 640,
"wires": []
},
{
"id": "3641660dd8894cd5",
"type": "debug",
"z": "5ec1131187731246",
"name": "zip input",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 600,
"wires": []
},
{
"id": "248320dfd65d34f6",
"type": "file",
"z": "5ec1131187731246",
"name": "",
"filename": "filename",
"filenameType": "msg",
"appendNewline": false,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 940,
"y": 720,
"wires": [
[]
]
},
{
"id": "152de0ddadb83a06",
"type": "catch",
"z": "5ec1131187731246",
"name": "zip errors?",
"scope": [
"487ca486.c231ac"
],
"uncaught": false,
"x": 580,
"y": 780,
"wires": [
[
"a273e46259506636",
"450dfb0216c54154"
]
]
},
{
"id": "a273e46259506636",
"type": "debug",
"z": "5ec1131187731246",
"name": "zip errors",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "error",
"targetType": "msg",
"statusVal": "error.message",
"statusType": "msg",
"x": 940,
"y": 780,
"wires": []
},
{
"id": "255e4104ae3dad7d",
"type": "change",
"z": "5ec1131187731246",
"name": "",
"rules": [
{
"t": "move",
"p": "error",
"pt": "msg",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 760,
"y": 840,
"wires": [
[
"b39272e50866b262"
]
]
},
{
"id": "d5f8028683a4a7d3",
"type": "http response",
"z": "5ec1131187731246",
"name": "",
"statusCode": "",
"headers": {
"content-type": "application/zip"
},
"x": 930,
"y": 680,
"wires": []
},
{
"id": "450dfb0216c54154",
"type": "switch",
"z": "5ec1131187731246",
"name": "msg.res?",
"property": "res",
"propertyType": "msg",
"rules": [
{
"t": "istype",
"v": "object",
"vt": "object"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 740,
"y": 800,
"wires": [
[
"255e4104ae3dad7d"
]
]
}
]