Compare commits
No commits in common. "8cae9076bd9fc2eacc409c5b6af181cd1eeeef8d" and "18873337b15de5780b0ba28313160d99da092185" have entirely different histories.
8cae9076bd
...
18873337b1
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
|
@ -1,252 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="380.13333mm"
|
|
||||||
height="110.25833mm"
|
|
||||||
viewBox="0 0 380.13334 110.25833"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
|
||||||
sodipodi:docname="Преобразования задач.svg"
|
|
||||||
inkscape:export-filename="/home/pavel/obsidian/Личные/Проекты/Монолитный Redmine Event Emitter/Документация/_resources/Преобразования задач.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<defs
|
|
||||||
id="defs2">
|
|
||||||
<marker
|
|
||||||
style="overflow:visible;"
|
|
||||||
id="marker1241"
|
|
||||||
refX="0.0"
|
|
||||||
refY="0.0"
|
|
||||||
orient="auto"
|
|
||||||
inkscape:stockid="Arrow2Lend"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
transform="scale(1.1) rotate(180) translate(1,0)"
|
|
||||||
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
|
|
||||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
|
||||||
id="path1239" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
style="overflow:visible;"
|
|
||||||
id="marker1213"
|
|
||||||
refX="0.0"
|
|
||||||
refY="0.0"
|
|
||||||
orient="auto"
|
|
||||||
inkscape:stockid="Arrow2Lend"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
transform="scale(1.1) rotate(180) translate(1,0)"
|
|
||||||
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
|
|
||||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
|
||||||
id="path1211" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
style="overflow:visible;"
|
|
||||||
id="Arrow2Lend"
|
|
||||||
refX="0.0"
|
|
||||||
refY="0.0"
|
|
||||||
orient="auto"
|
|
||||||
inkscape:stockid="Arrow2Lend"
|
|
||||||
inkscape:isstock="true"
|
|
||||||
inkscape:collect="always">
|
|
||||||
<path
|
|
||||||
transform="scale(1.1) rotate(180) translate(1,0)"
|
|
||||||
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
|
|
||||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
|
||||||
id="path918" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
style="overflow:visible;"
|
|
||||||
id="Arrow2Mend"
|
|
||||||
refX="0.0"
|
|
||||||
refY="0.0"
|
|
||||||
orient="auto"
|
|
||||||
inkscape:stockid="Arrow2Mend"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
transform="scale(0.6) rotate(180) translate(0,0)"
|
|
||||||
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
|
|
||||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
|
||||||
id="path924" />
|
|
||||||
</marker>
|
|
||||||
<rect
|
|
||||||
x="124.35417"
|
|
||||||
y="15.875"
|
|
||||||
width="105.83333"
|
|
||||||
height="31.75"
|
|
||||||
id="rect879" />
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="1"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.98994949"
|
|
||||||
inkscape:cx="755.38262"
|
|
||||||
inkscape:cy="192.02171"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="true"
|
|
||||||
inkscape:window-width="1856"
|
|
||||||
inkscape:window-height="1051"
|
|
||||||
inkscape:window-x="64"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:snap-text-baseline="true"
|
|
||||||
fit-margin-top="10"
|
|
||||||
lock-margins="true"
|
|
||||||
fit-margin-left="10"
|
|
||||||
fit-margin-right="10"
|
|
||||||
fit-margin-bottom="10">
|
|
||||||
<inkscape:grid
|
|
||||||
type="xygrid"
|
|
||||||
id="grid36"
|
|
||||||
originx="2.2125001"
|
|
||||||
originy="52.483332" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Слой 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(2.2125,52.483332)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:85.6522;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="22.545856"
|
|
||||||
y="31.792259"
|
|
||||||
id="text30"
|
|
||||||
transform="translate(-6.6699219,-2.6880925)"><tspan
|
|
||||||
x="22.545856"
|
|
||||||
y="31.792259"><tspan
|
|
||||||
style="stroke-width:0.3">Чтение данных из </tspan></tspan><tspan
|
|
||||||
x="22.545856"
|
|
||||||
y="39.729758"><tspan
|
|
||||||
style="stroke-width:0.3">redmine </tspan><tspan
|
|
||||||
style="stroke-width:0.3">через </tspan><tspan
|
|
||||||
style="stroke-width:0.3">http api</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
|
|
||||||
id="rect38"
|
|
||||||
width="100.54166"
|
|
||||||
height="31.75"
|
|
||||||
x="7.9375"
|
|
||||||
y="15.875" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="132.29167"
|
|
||||||
y="37.041668"
|
|
||||||
id="text865"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan863"
|
|
||||||
x="132.29167"
|
|
||||||
y="37.041668"
|
|
||||||
style="stroke-width:0.3" /></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6673;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="18.520834"
|
|
||||||
y="-7.9375"
|
|
||||||
id="text869"
|
|
||||||
transform="translate(-2.6464845,-23.812501)"><tspan
|
|
||||||
x="18.520834"
|
|
||||||
y="-7.9375"><tspan
|
|
||||||
style="stroke-width:0.3">Указание коллекции </tspan></tspan><tspan
|
|
||||||
x="18.520834"
|
|
||||||
y="2.970919e-07"><tspan
|
|
||||||
style="stroke-width:0.3">функций-</tspan></tspan><tspan
|
|
||||||
x="18.520834"
|
|
||||||
y="7.9375003"><tspan
|
|
||||||
style="stroke-width:0.3">преобразователей при </tspan></tspan><tspan
|
|
||||||
x="18.520834"
|
|
||||||
y="15.875"><tspan
|
|
||||||
style="stroke-width:0.3">инициализии приложения</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:1.20000006,1.20000006;stroke-dashoffset:0"
|
|
||||||
id="rect871"
|
|
||||||
width="100.54166"
|
|
||||||
height="42.333332"
|
|
||||||
x="7.9375"
|
|
||||||
y="-42.333332" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:119.062;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="132.29167"
|
|
||||||
y="29.104166"
|
|
||||||
id="text875"><tspan
|
|
||||||
x="132.29167"
|
|
||||||
y="29.104166"><tspan
|
|
||||||
style="stroke-width:0.3">Выполнение всех функций-</tspan></tspan><tspan
|
|
||||||
x="132.29167"
|
|
||||||
y="37.041666"><tspan
|
|
||||||
style="stroke-width:0.3">преобразователей</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
id="text877"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect879);fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000;" />
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
|
|
||||||
id="rect883"
|
|
||||||
width="108.47916"
|
|
||||||
height="31.75"
|
|
||||||
x="124.35416"
|
|
||||||
y="15.875" />
|
|
||||||
<path
|
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.30000001;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:1.20000006,1.20000006;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1;marker-end:url(#Arrow2Lend)"
|
|
||||||
d="m 108.47917,-23.8125 c 39.6875,0 63.5,10.583333 63.49999,39.6875"
|
|
||||||
id="path885"
|
|
||||||
sodipodi:nodetypes="cc" />
|
|
||||||
<path
|
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1;marker-end:url(#marker1241)"
|
|
||||||
d="m 108.47917,31.75 h 15.875"
|
|
||||||
id="path887" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:113.771;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="261.9375"
|
|
||||||
y="29.10417"
|
|
||||||
id="text891"
|
|
||||||
transform="translate(-5.2916514)"><tspan
|
|
||||||
x="261.9375"
|
|
||||||
y="29.10417"><tspan
|
|
||||||
style="stroke-width:0.3">Дальнейшая работа с данными </tspan></tspan><tspan
|
|
||||||
x="261.9375"
|
|
||||||
y="37.04167"><tspan
|
|
||||||
style="stroke-width:0.3">(сохранение в </tspan><tspan
|
|
||||||
style="stroke-width:0.3">CouchDB</tspan><tspan
|
|
||||||
style="stroke-width:0.3">)</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
|
|
||||||
id="rect893"
|
|
||||||
width="119.0625"
|
|
||||||
height="31.75"
|
|
||||||
x="248.70833"
|
|
||||||
y="15.875" />
|
|
||||||
<path
|
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1;marker-end:url(#marker1213)"
|
|
||||||
d="m 232.83333,31.749999 h 15.875"
|
|
||||||
id="path895" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
|
@ -1,462 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="265.65228mm"
|
|
||||||
height="517.26074mm"
|
|
||||||
viewBox="0 0 265.65229 517.26075"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
|
||||||
sodipodi:docname="Сохранение в CouchDB.svg"
|
|
||||||
inkscape:export-filename="/home/pavel/obsidian/Личные/Проекты/Монолитный Redmine Event Emitter/Документация/_resources/Сохранение в CouchDB.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="1"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.49497475"
|
|
||||||
inkscape:cx="530.06227"
|
|
||||||
inkscape:cy="959.12713"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:window-width="928"
|
|
||||||
inkscape:window-height="1051"
|
|
||||||
inkscape:window-x="992"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:snap-text-baseline="true"
|
|
||||||
showguides="false"
|
|
||||||
fit-margin-top="10"
|
|
||||||
lock-margins="true"
|
|
||||||
fit-margin-left="10"
|
|
||||||
fit-margin-right="10"
|
|
||||||
fit-margin-bottom="10">
|
|
||||||
<inkscape:grid
|
|
||||||
type="xygrid"
|
|
||||||
id="grid28"
|
|
||||||
originx="49.837479"
|
|
||||||
originy="-14.118424" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="60.420812,512.85835"
|
|
||||||
orientation="300,0"
|
|
||||||
id="guide873" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="160.96248,436.12918"
|
|
||||||
orientation="110.00002,0"
|
|
||||||
id="guide885" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="92.170813,433.48335"
|
|
||||||
orientation="0,105"
|
|
||||||
id="guide889" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="71.004145,420.25418"
|
|
||||||
orientation="165,0"
|
|
||||||
id="guide927" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="171.54582,420.25418"
|
|
||||||
orientation="60,0"
|
|
||||||
id="guide931" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Слой 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(49.837477,-14.118424)">
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="-39.6875"
|
|
||||||
y="44.979164"
|
|
||||||
id="text857"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="-39.6875"
|
|
||||||
y="44.979164"
|
|
||||||
style="font-size:4.23333px;stroke-width:0.3"
|
|
||||||
id="tspan859">{</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="-39.6875"
|
|
||||||
y="50.270828"
|
|
||||||
style="font-size:4.23333px;stroke-width:0.3"
|
|
||||||
id="tspan861"> "_id": "8ccc",</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="-39.6875"
|
|
||||||
y="55.562489"
|
|
||||||
style="font-size:4.23333px;stroke-width:0.3"
|
|
||||||
id="tspan863"> "_rev": "0-abcd",</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="-39.6875"
|
|
||||||
y="60.854149"
|
|
||||||
style="font-size:4.23333px;stroke-width:0.3"
|
|
||||||
id="tspan865"> "field_a": "value_a"</tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="-39.6875"
|
|
||||||
y="66.145813"
|
|
||||||
style="font-size:4.23333px;stroke-width:0.3"
|
|
||||||
id="tspan867">}</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="10.583333"
|
|
||||||
y="97.895836"
|
|
||||||
id="text877"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan875"
|
|
||||||
x="10.583333"
|
|
||||||
y="97.895836"
|
|
||||||
style="stroke-width:0.3">Поток 1</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"
|
|
||||||
id="text881"
|
|
||||||
transform="translate(10.582682,5.2916667)"><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"><tspan
|
|
||||||
style="stroke-width:0.3">Чтение документа из </tspan></tspan><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="127"><tspan
|
|
||||||
style="stroke-width:0.3">коллекции</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="111.125"
|
|
||||||
y="97.895836"
|
|
||||||
id="text893"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan891"
|
|
||||||
x="111.125"
|
|
||||||
y="97.895836"
|
|
||||||
style="stroke-width:0.3">Поток 2</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#008000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"
|
|
||||||
id="text901"
|
|
||||||
transform="translate(6.5094167e-4,3.9687551)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3">{
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="158.74999"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> id: 8ccc,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="164.04165"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> rev: 0-abcd,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="169.3333"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> field_a: value_a
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="174.62496"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3">}</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect903"
|
|
||||||
width="5.2916665"
|
|
||||||
height="95.250008"
|
|
||||||
x="10.583333"
|
|
||||||
y="111.125" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"
|
|
||||||
id="text881-6"
|
|
||||||
transform="translate(111.12435,44.979173)"><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"><tspan
|
|
||||||
style="stroke-width:0.3">Чтение документа из </tspan></tspan><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="127"><tspan
|
|
||||||
style="stroke-width:0.3">коллекции</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect903-5"
|
|
||||||
width="5.2916665"
|
|
||||||
height="140.22917"
|
|
||||||
x="111.125"
|
|
||||||
y="150.8125" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"
|
|
||||||
id="text881-3"
|
|
||||||
transform="translate(10.582683,124.35415)"><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"><tspan
|
|
||||||
style="stroke-width:0.3">Запись новой версии </tspan></tspan><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="127"><tspan
|
|
||||||
style="stroke-width:0.3">документа в </tspan><tspan
|
|
||||||
style="stroke-width:0.3">коллекцию</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"
|
|
||||||
id="text901-5"
|
|
||||||
transform="translate(6.4991001e-4,113.77082)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"><tspan
|
|
||||||
style="font-size:4.23333px">{
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="158.74999"><tspan
|
|
||||||
style="font-size:4.23333px"> id: 8ccc,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="164.04165"><tspan
|
|
||||||
style="font-size:4.23333px"> rev: 0-abcd,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="169.3333"><tspan
|
|
||||||
style="font-size:4.23333px"> </tspan><tspan
|
|
||||||
style="font-size:4.23333px;fill:#0000ff">field_a: success_value</tspan><tspan
|
|
||||||
style="font-size:4.23333px">
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="174.62496"><tspan
|
|
||||||
style="font-size:4.23333px">}</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect903-6"
|
|
||||||
width="5.2916665"
|
|
||||||
height="137.58333"
|
|
||||||
x="10.583333"
|
|
||||||
y="230.1875" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#008000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="333.375"
|
|
||||||
id="text1140"
|
|
||||||
transform="translate(6.4991001e-4,-26.458354)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="333.375"><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">Успех. Документу </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="341.31251"><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">внутри </tspan><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">CouchDB </tspan><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">будет </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="349.25001"><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">проставлена новая </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="357.18751"><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">ревизия - 1-</tspan><tspan
|
|
||||||
style="fill:#008000;stroke-width:0.3">bcde</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="-39.6875"
|
|
||||||
y="29.104166"
|
|
||||||
id="text1260"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan1258"
|
|
||||||
x="-39.6875"
|
|
||||||
y="29.104166"
|
|
||||||
style="stroke-width:0.3">Исходный документ:</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#008000;stroke:none;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="148.16667"
|
|
||||||
id="text1268"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan1266"
|
|
||||||
x="21.166666"
|
|
||||||
y="148.16667"
|
|
||||||
style="fill:#008000;stroke:none;stroke-width:0.3">Получили:</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#008000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"
|
|
||||||
id="text901-5-0"
|
|
||||||
transform="translate(6.4964419e-4,187.85415)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000">{
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="158.74999"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000"> id: 8ccc,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="164.04165"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000"> rev: 1-bcde,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="169.3333"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000"> </tspan><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000">field_a: success_value</tspan><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000">
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="174.62496"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000">}</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"
|
|
||||||
id="text881-3-9"
|
|
||||||
transform="translate(111.12433,285.74998)"><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="119.0625"><tspan
|
|
||||||
style="stroke-width:0.3">Запись новой версии </tspan></tspan><tspan
|
|
||||||
x="10.583333"
|
|
||||||
y="127"><tspan
|
|
||||||
style="stroke-width:0.3">документа в </tspan><tspan
|
|
||||||
style="stroke-width:0.3">коллекцию</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"
|
|
||||||
id="text901-5-3"
|
|
||||||
transform="translate(100.5423,275.16665)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"><tspan
|
|
||||||
style="font-size:4.23333px">{
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="158.74999"><tspan
|
|
||||||
style="font-size:4.23333px"> id: 8ccc,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="164.04165"><tspan
|
|
||||||
style="font-size:4.23333px"> rev: 0-abcd,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="169.3333"><tspan
|
|
||||||
style="font-size:4.23333px"> </tspan><tspan
|
|
||||||
style="font-size:4.23333px;fill:#0000ff">field_a: fail_value</tspan><tspan
|
|
||||||
style="font-size:4.23333px">
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="174.62496"><tspan
|
|
||||||
style="font-size:4.23333px">}</tspan></tspan></text>
|
|
||||||
<rect
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.300001;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect903-6-6"
|
|
||||||
width="5.2916665"
|
|
||||||
height="124.35415"
|
|
||||||
x="111.125"
|
|
||||||
y="391.58337" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#ff0000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="333.375"
|
|
||||||
id="text1140-0"
|
|
||||||
transform="translate(100.5423,134.93748)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="333.375"><tspan
|
|
||||||
style="fill:#ff0000">Fail. </tspan><tspan
|
|
||||||
style="fill:#ff0000">Ревизия не </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="341.31251"><tspan
|
|
||||||
style="fill:#ff0000">совпадает с предыдущим </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="349.25001"><tspan
|
|
||||||
style="fill:#ff0000">значением:
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="357.18751"><tspan
|
|
||||||
style="fill:#ff0000">"0-</tspan><tspan
|
|
||||||
style="fill:#ff0000">abcd" != "1-bcde".
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="365.12501"><tspan
|
|
||||||
style="fill:#ff0000">Документ не будет </tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="373.06251"><tspan
|
|
||||||
style="fill:#ff0000">обновлён.</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:4.23333px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;white-space:pre;inline-size:84.6667;fill:#008000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"
|
|
||||||
id="text901-2"
|
|
||||||
transform="translate(100.54233,43.656245)"><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="153.45833"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3">{
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="158.74999"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> id: 8ccc,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="164.04165"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> rev: 0-abcd,
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="169.3333"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3"> field_a: value_a
|
|
||||||
</tspan></tspan><tspan
|
|
||||||
x="21.166666"
|
|
||||||
y="174.62496"><tspan
|
|
||||||
style="font-size:4.23333px;fill:#008000;stroke-width:0.3">}</tspan></tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#008000;stroke:none;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="121.70834"
|
|
||||||
y="187.85417"
|
|
||||||
id="text1268-6"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan1266-1"
|
|
||||||
x="121.70834"
|
|
||||||
y="187.85417"
|
|
||||||
style="fill:#008000;stroke:none;stroke-width:0.3">Получили:</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-size:6.35px;line-height:normal;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';text-decoration:none;text-decoration-line:none;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
|
|
||||||
x="-39.6875"
|
|
||||||
y="97.895836"
|
|
||||||
id="text1634"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan1632"
|
|
||||||
x="-39.6875"
|
|
||||||
y="97.895836"
|
|
||||||
style="stroke-width:0.3">Время</tspan></text>
|
|
||||||
<path
|
|
||||||
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1"
|
|
||||||
d="m -34.395832,111.125 h 5.291666 v 402.16666 h 5.291666 l -7.937498,7.9375 -7.9375,-7.9375 h 5.291666 z"
|
|
||||||
id="path1636" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,52 +0,0 @@
|
||||||
# Общая структура
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Основной код приложения написан на TypeScript и фреймворке NestJS (это такой Angular для backend-а)
|
|
||||||
|
|
||||||
Монолитное приложение включает в своём составе:
|
|
||||||
|
|
||||||
- основная функциональность
|
|
||||||
- кеш и промежуточный коллектор для данных на couchdb
|
|
||||||
- http api для внешних интеграций
|
|
||||||
- webhook-и и websocket-ы
|
|
||||||
- telegram-бот - есть возможность для реализации новых интеграций с другими чат-ботами
|
|
||||||
- cron-task-и для выполнения повторяющихся задач
|
|
||||||
- frontend server-side на простой шаблонизации
|
|
||||||
- frontend на react
|
|
||||||
- специальный плагин для lowcode платформы n8n
|
|
||||||
|
|
||||||
# Основной процесс
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Двигателем приложения служит redmine-issue-event-emitter - это основной процесс поступления данных в Pinkmine.
|
|
||||||
|
|
||||||
1. Подписка на источники событий - по email, через cron-таски, через rss. Позволяет обнаруживать факты изменений в задачах в redmine. ([подробнее](Как%20это%20работает/Как%20работают%20стратегии%20синхронизации%20redmine%20и%20pinkmine.md))
|
|
||||||
2. Redmine Issue Event Emitter - это очередь для синхронизации данных из redmine. На выход кладётся номер задачи, на выходе данные полученные с помощью redmine api. Очередь обеспечивает бережное отношение к боевому redmine, так как позволяет выполнять выгрузки не выходя за выставленный лимит обращений. ([подробнее про работу очереди](Как%20это%20работает/Как%20работает%20очередь%20загрузки%20задач.md) и [подробнее про загрузку задач из redmine](Как%20это%20работает/Как%20происходит%20загрузка%20задачи%20из%20Redmine.md))
|
|
||||||
3. Преобразование задачи - это набор вспомогательных обработчиков. Позволяет при получении данных произвести попутно полезные изменения, например, привести текстовые теги к массиву, определить текущего ответственного за задачу, обнаружить в описании или в комментариях ссылки на gitlab на MR-ы, в т.ч. можно реализовать специализированный обработчик. ([подбронее](./Как%20это%20работает/Как%20происходит%20преобразование%20задачи.md))
|
|
||||||
4. Сохранение задачи в кеш в CouchDB ([подробнее](./Как%20это%20работает/Сохранение%20задачи%20в%20кеш%20CouchDB.md))
|
|
||||||
5. Анализ изменений в задаче - это сравнение предыдущего состояния задачи с новым. Позволяет организовать процессы связанные с жизненным циклом самой задачи и, например, разослать уведомления с учётом происходящих изменений ([подробнее](./Как%20это%20работает/Анализ%20изменений%20в%20задаче.md))
|
|
||||||
6. Сохранение событий изменения в журнал - вспомогательная коллекция данных ([подробнее](./Как%20это%20работает/Анализ%20изменений%20в%20задаче.md))
|
|
||||||
|
|
||||||
# Точки расширения
|
|
||||||
|
|
||||||
Платформа Pinkmine предполагает в т.ч. реализацию конкретных задач в рамках основного кода. Допускается реализация специализированных внутренних сервисов.
|
|
||||||
|
|
||||||
В рамках ядра redmine-issue-event-emitter:
|
|
||||||
|
|
||||||
- новые стратегии получения задач из Redmine
|
|
||||||
- специализированные обработчики задач для отдельных проектов
|
|
||||||
- интеграция событий в новые процессы - чат-боты или простая передача данных с помощью webhook-ов и websocket-ов
|
|
||||||
|
|
||||||
В рамках расширенной платформы pinkmine:
|
|
||||||
|
|
||||||
Благодаря CouchDB стало возможным предоставить api для поиска задач с более гибкими условиями для выборки чем во встроенном в redmine api. Можно использовать в выборках комбинированные выражения через "и"/"или". Богатый набор операторов - "есть значение", "нет значения", "больше", "меньше", "равно", "поиск по регулярному выражению". Для поиска можно пользоваться расширенными данными.
|
|
||||||
|
|
||||||
CouchDB позволяет выгружать для работы и анализа больший объём данных чем основной api redmine.
|
|
||||||
|
|
||||||
Webhook-и и websocket-ы могут сделать api ещё более гибким.
|
|
||||||
|
|
||||||
В рамках реализованного telegram chat бота можно дописывать логику работы под узкоспециализированные задачи.
|
|
||||||
|
|
||||||
С помощью фреймворка NestJS можно сделать сервисы для решения узких задач. Например, сделать cron-task-у с аналитической выгрузкой с сохранением промежуточных результатов во вспомогательную коллекцию в CouchDB и предостатить уже к ней доступ через http api и сделать человеко-читаемое представление с помощью шаблона.
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
Как было описано в документе [Сохранение задачи в кеш CouchDB](./Сохранение%20задачи%20в%20кеш%20CouchDB.md) специфика работы с данными из issue такова что есть два состояния - предыдущее и текущее состояние.
|
|
||||||
|
|
||||||
Значит есть возможность сравнения двух состояний и построение дополнительной логики на основе сравнения.
|
|
||||||
|
|
||||||
Это позволило реализовать рассылку уведомлений при обнаружении в задачах изменений.
|
|
||||||
|
|
||||||
Функции сохранения данных в CouchDB с вычислением изменений реализована в `libs/event-emitter/src/issue-cache-writer/redmine-issues-cache-writer.service.ts`.
|
|
||||||
|
|
||||||
Затем с помощью пайплайнов rxjs реализован дополнительный анализ изменений:
|
|
||||||
|
|
||||||
1. `src/notifications/personal-notifications.service.ts` - поиск упоминаний пользователей redmine в комментариях для пересылки личных уведомлений в чат-бот
|
|
||||||
2. `src/notifications/status-change-notifications.service.ts` - поиск изменений статусов для рассылки уведомлений согласно изменениям ответственного за дальнейшую работу над задачей
|
|
||||||
3. `src/changes-cache-writer/changes-cache-writer.service.ts` - сохранение изменений в дополнительный журнал активностей в коллекцию в CouchDB
|
|
||||||
|
|
||||||
Сам пайплайн rxjs определяется в инициализирующей функции в `src/app.module.ts`.
|
|
||||||
|
|
||||||
Для правильности работы функций анализа нужно выполнить правильно конфигугирование.
|
|
||||||
|
|
||||||
Статусы используемые в вашем экземпляре redmine определяются в конфигурационном файле `configs/redmine-statuses-config.jsonc`. Они имеет следующий вид:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "New"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "In Progress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"name": "Closed",
|
|
||||||
"is_closed": true
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Идентификаторы и name должны соответствовать статусам настроенным в основном redmine.
|
|
||||||
|
|
||||||
Правила для определения изменений в статусах определяются с помощью конфигурационного файла `configs/redmine-status-changes-config.jsonc`. Он имеет следующий вид:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"default": false,
|
|
||||||
"from": "New",
|
|
||||||
"to": "In Progress",
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"recipient": "<field_name>",
|
|
||||||
// Handlebars - template engine
|
|
||||||
"changes_message": "{{qa.name}} got issue #{{issue_id}} after development {{dev.name}}",
|
|
||||||
"notification_message": "<a href=\"{{ issue_url }}\">{{ issue_tracker }} #{{ issue_id }}</a> {{ issue_subject }}:\n{{dev.name}} finished development. You can test issue.\n\n{{journal_note}}"
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Это набор правил для формирования уведомлений и сообщений для журнала изменений. Поля "from" и "to" - указывают на возможные статусы задачи. "messages" - это набор сообщений. Тут можно определить варианты сообщений для различных получателей задаваемых полем "recipient", а тексты сообщений с помощью шаблонизатора [Handlebars](https://handlebarsjs.com).
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
Это обращение к стандартной функции через http api по следующему шаблону:
|
|
||||||
|
|
||||||
`<redmine_url>/issues/<issue_number>.json?include=children,journals,relations`
|
|
||||||
|
|
||||||
Подробное описание параметров api можно найти в официальной фокументации на странице [Rest_Issues](https://www.redmine.org/projects/redmine/wiki/Rest_Issues)
|
|
||||||
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
Для разрешения некоторых проблем напрашивалось попутно после получения данных об issue из redmine делать преобразования
|
|
||||||
|
|
||||||
Примеры:
|
|
||||||
|
|
||||||
1. Преобразовать дату+вермя из текстового формата в числовой для возможности фильтрации данных с помощью сравнивающих операторов - больше и меньше.
|
|
||||||
2. Привести текстовое значение тегов к массиву для фильтрации данных с помощью оператора "$in".
|
|
||||||
3. Можно извлечь из описания и комментариев ссылки на MR-ы в gitlab-е.
|
|
||||||
4. Определить текущего ответственного за задачу по правилам соответствия статусу одному из полей - "Назначено", "Code Reviewer", "Quality Assurance".
|
|
||||||
|
|
||||||
Через конфигурационный файл уже не решить настройку, т.к. за преобразования задач отвечают функции - а если точнее то классы с реализацией интерфейса `IssueEnhancerInterface`. Это делается в коде проекта Pinkmine. Примеры реализаций в файлах:
|
|
||||||
|
|
||||||
- `libs/event-emitter/src/issue-enhancers/timestamps-enhancer.ts` - преобразует дату+время из текстового формата в числовой
|
|
||||||
- `libs/event-emitter/src/issue-enhancers` - в папке ещё несколько универсальных примеров (не зависящих от экземпляра redmine)
|
|
||||||
- `src/issue-enhancers` - в папке лежат дополнительные примеры с функциями уже привязанными к рабочему redmine и специфичные для рабочих проектов
|
|
||||||
|
|
||||||
Чтобы эти функции отрабатывали налету после получения данных из redmine и перед сохранением в кеш, то нужно при начальной инициализации приложения (`src/app.module.ts` > `AppModule.onModuleInit`) указать набор реализаций `IssueEnhancerInterface`. Упрощённый код, отражающий суть:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class AppModule {
|
|
||||||
// ...
|
|
||||||
onModuleInit() {
|
|
||||||
// ...
|
|
||||||
this.enhancerService.addEnhancer([
|
|
||||||
this.timestampEnhancer,
|
|
||||||
this.customFieldsEnhancer,
|
|
||||||
this.currentUserEnhancer,
|
|
||||||
this.issueUrlEnhancer,
|
|
||||||
this.categoryMergeToTagsEnhancer,
|
|
||||||
this.calendarEnhancer,
|
|
||||||
]);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Схематически процесс можно проиллюстрировать так:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Выполнение преобразований данных до сохранения в кеш в CouchDB позволяет в последствии использовать дополнительные данные для выборки задач
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
Это стандартный паттерн:
|
|
||||||
|
|
||||||
1. берём максимум N номеров задач раз в M секунд,
|
|
||||||
2. передаём их в функцию для синхронизаицации
|
|
||||||
1. выгружаем данные о задаче из redmine api
|
|
||||||
2. пишем полученные данные во внутренний кеш
|
|
||||||
|
|
||||||
Очередь нужна для бережного отношения к боевому redmine, потому что не загрузит CPU на сервере сотнями одновременных параллельных запросов.
|
|
||||||
|
|
||||||
Настройка очереди производится в конфигурационном файле `configs/issue-event-emitter-config.jsonc` в секции `issueChangesQueue`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
// ...
|
|
||||||
"issueChangesQueue": {
|
|
||||||
"updateInterval": 5000, // 5 sec
|
|
||||||
"itemsLimit": 3
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
# Стратегия по email
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. В задаче в redmine делается любое изменение - смена статуса, обновление описания, добавление нового комментария.
|
|
||||||
2. Redmine отправляет сообщение о факте обновления задачи по email
|
|
||||||
3. Pinkmine читает новые письма протоколу imap, находит в заголовках номер задачи с помощью регулярного выражения
|
|
||||||
4. Pinkmine помещает номер задачи в очередь для выполнения обновления
|
|
||||||
|
|
||||||
Параметры работы стратегии задаются в конфигурационном файле `configs/issue-event-emitter-config.jsonc` в секции `mailListener`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
"mailListener": {
|
|
||||||
|
|
||||||
// регулярное выражение для определения номера задачи в заголовке письма:
|
|
||||||
"issueNumberParser": "\\b(?<=#)\\d+\\b",
|
|
||||||
|
|
||||||
// параметры для доступа к почте через протокол imap:
|
|
||||||
"imapSimpleConfig": {
|
|
||||||
"imap": {
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"host": "",
|
|
||||||
"port": 143,
|
|
||||||
// tls: true,
|
|
||||||
"autotls": "always",
|
|
||||||
"authTimeout": 5000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// интервал через который происходит проверка почты:
|
|
||||||
"updateInterval": 180000, // 3 min
|
|
||||||
|
|
||||||
// имя папки в которой следует искать входящие письма
|
|
||||||
"boxName": "INBOX"
|
|
||||||
},
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Стратегия через cron-таски
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
1. Pinkmine загружает данные из преднастроенных query-запросов в redmine в формате csv. Выбирает данные по двум полям - id задачи и "обновлено" (дата+время обновления задачи)
|
|
||||||
2. Pinkmine проверяет какие задачи в своём кеше (CouchDB) уже успели устареть
|
|
||||||
3. Номера устаревших задач кладёт в очередь для дальнейшего обновления
|
|
||||||
|
|
||||||
Параметры работы стратегии задаются в конфигурационном файле `configs/issue-event-emitter-config.jsonc` в секциях `csvListener` и `rootIssueListener`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// таски для синхронизации по преднастроенным запросам:
|
|
||||||
"csvListener": {
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
// расписание для выполнения задачи (синтаксис crontab)
|
|
||||||
"schedule": "* * * * *",
|
|
||||||
|
|
||||||
// поле в csv-файле с датой и временем обновления задачи
|
|
||||||
"updatedAtFieldName": "Обновлено",
|
|
||||||
|
|
||||||
// формат даты и времени
|
|
||||||
"dateTimeFormat": "dd.MM.yyyy HH:mm",
|
|
||||||
|
|
||||||
// ссылки на предстароенные запросы выборки задач в формате csv
|
|
||||||
"csvLinks": [
|
|
||||||
"https://<redmine_host>/projects/proj/issues.csv?utf8=%E2%9C%93&query_id=2"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// таски для синхронизации корневых и суммирующих задач:
|
|
||||||
"rootIssueListener": {
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
// расписание для выполнения задачи (синтаксис crontab)
|
|
||||||
"schedule": "15 6,12 * * *",
|
|
||||||
|
|
||||||
// номера задач
|
|
||||||
"rootIssues": [
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
7,
|
|
||||||
11
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Эта стратегия позволяет догружать данные, которые не пришли по email рассылке: закрытые задачи, shady-mode, изменение структуры дерева при смене привязки к родительской задаче и т.п.
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
CouchDB - это nosql субд. Основная концепция CouchDB - это коллекции данных (json) с возможностью гибкого поиска по ним.
|
|
||||||
|
|
||||||
Важная особенность CouchDB - это отсутствие механизма транзакций. Гарантия согласованности записи обеспечивается с помощью контроля ревизии.
|
|
||||||
|
|
||||||
Документы в CouchDB имеют версионирование, аналогичное тому, как это было бы в обычной системе контроля версий, такой как Subversion. Если вы хотите изменить значение в документе, вы создаете полностью новую версию этого документа и сохраняете ее поверх старой. После выполнения этого вы получите две версии одного и того же документа, одну старую и одну новую.
|
|
||||||
|
|
||||||
Как это обеспечивает улучшение по сравнению с блокировками? Рассмотрим набор запросов, желающих получить доступ к документу. Первый запрос считывает документ. Пока он обрабатывается, второй запрос изменяет документ. Поскольку второй запрос включает совершенно новую версию документа, CouchDB может просто добавить его в базу данных, не дожидаясь завершения запроса на чтение.
|
|
||||||
|
|
||||||
Когда третий запрос захочет прочитать тот же документ, CouchDB укажет ему на новую версию, которая только что была написана. В течение всего этого процесса первый запрос все еще может читать исходную версию.
|
|
||||||
|
|
||||||
Запрос на чтение всегда будет отображать самый последний снимок вашей базы данных на момент начала запроса.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Redmine Issue Event Emitter всегда для записи нового состояния задачи должен прочитать предыдущее состояние. Это обязательно нужно для корректного определения последней ревизии. Одновременно для дальнейшего анализа станет доступно два состояния - предыдущее и текущее. Благодаря этой особенности работы с CouchDB можно проводить дополнительный [анализ произошедших в задаче изменений](./Анализ%20изменений%20в%20задаче.md).
|
|
||||||
|
|
||||||
Настройка доступа к CouchDB делается в конфигурационном файле `configs/issue-event-emitter-config.jsonc` в секции `couchDb`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
// ...
|
|
||||||
"couchDb": {
|
|
||||||
"url": "http://admin:password@localhost:5984",
|
|
||||||
"dbs": {
|
|
||||||
"users": "redmine_users",
|
|
||||||
"issues": "redmine_issues",
|
|
||||||
"dashboards": "dashboards"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -183,13 +183,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
subj.subscribe(async (issues: RedmineTypes.Issue[]) => {
|
subj.subscribe(async (issues: RedmineTypes.Issue[]) => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Changed issues - ` +
|
`Changed issues - ` +
|
||||||
issues.map(
|
issues.map((i) => `#${i.id} (${i.subject})`).join(', '),
|
||||||
(i) => {
|
|
||||||
return (i && i.id && i.subject)
|
|
||||||
? `#${i.id} (${i.subject})`
|
|
||||||
: '';
|
|
||||||
}
|
|
||||||
).join(', '),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < issues.length; i++) {
|
for (let i = 0; i < issues.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,7 @@ export class RedmineEventsGateway {
|
||||||
if (this.listeners && this.listeners.length > 0) {
|
if (this.listeners && this.listeners.length > 0) {
|
||||||
this.getIssuesChangesQueue().queue.subscribe((data) => {
|
this.getIssuesChangesQueue().queue.subscribe((data) => {
|
||||||
const issues = data.map((issue) => {
|
const issues = data.map((issue) => {
|
||||||
return (issue && issue.id && issue.subject)
|
return `${issue['id']} - ${issue['subject']}`;
|
||||||
? `${issue['id']} - ${issue['subject']}`
|
|
||||||
: '';
|
|
||||||
});
|
});
|
||||||
this.logger.debug('Changed issues: ' + JSON.stringify(issues));
|
this.logger.debug('Changed issues: ' + JSON.stringify(issues));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,6 @@ export class IssuesController {
|
||||||
return await this.issuesService.find(params);
|
return await this.issuesService.find(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('find-from-root/:id')
|
|
||||||
async getIssuesFromRoot(
|
|
||||||
@Param('id') id: number,
|
|
||||||
): Promise<RedmineTypes.Issue[]> {
|
|
||||||
const rootIssue = await this.issuesService.getIssue(id);
|
|
||||||
const res = await this.issuesService.getIssuesFromRoot(rootIssue);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('find-from-merged-trees-and-query')
|
|
||||||
async findFromMergedTreesAndQuery(
|
|
||||||
@Body() params: any,
|
|
||||||
): Promise<RedmineTypes.Issue[]> {
|
|
||||||
return await this.issuesService.mergedTreesAndFind(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async getIssue(@Param('id') id: number): Promise<RedmineTypes.Issue> {
|
async getIssue(@Param('id') id: number): Promise<RedmineTypes.Issue> {
|
||||||
return await this.issuesService.getIssue(id);
|
return await this.issuesService.getIssue(id);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import nano from 'nano';
|
||||||
import { UNLIMITED } from '../consts/consts';
|
import { UNLIMITED } from '../consts/consts';
|
||||||
import { GetParentsHint } from '../utils/get-parents-hint';
|
import { GetParentsHint } from '../utils/get-parents-hint';
|
||||||
import { TreeIssuesStore } from '../utils/tree-issues-store';
|
import { TreeIssuesStore } from '../utils/tree-issues-store';
|
||||||
import { FlatIssuesStore } from '../utils/flat-issues-store';
|
|
||||||
|
|
||||||
export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
|
export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
|
||||||
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
|
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
|
||||||
|
|
@ -19,12 +18,6 @@ export namespace IssuesServiceNs {
|
||||||
export type IssuesLoader = (
|
export type IssuesLoader = (
|
||||||
ids: number[],
|
ids: number[],
|
||||||
) => Promise<Record<number, RedmineTypes.Issue | null>>;
|
) => Promise<Record<number, RedmineTypes.Issue | null>>;
|
||||||
|
|
||||||
export type TreesAndQuery = {
|
|
||||||
rootIds?: number[];
|
|
||||||
rootIssues?: RedmineTypes.Issue[];
|
|
||||||
query?: nano.MangoQuery;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -183,43 +176,4 @@ export class IssuesService {
|
||||||
await treeIssuesStore.fillData(loader);
|
await treeIssuesStore.fillData(loader);
|
||||||
return treeIssuesStore.getIssuesWithChildren();
|
return treeIssuesStore.getIssuesWithChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIssuesFromRoot(
|
|
||||||
rootIssue: RedmineTypes.Issue,
|
|
||||||
): Promise<RedmineTypes.Issue[]> {
|
|
||||||
const treeStore = new TreeIssuesStore();
|
|
||||||
treeStore.setRootIssue(rootIssue);
|
|
||||||
const loader = this.createDynamicIssuesLoader();
|
|
||||||
await treeStore.fillData(loader);
|
|
||||||
return treeStore.getFlatStore().getIssues();
|
|
||||||
}
|
|
||||||
|
|
||||||
async mergedTreesAndFind(
|
|
||||||
query: IssuesServiceNs.TreesAndQuery,
|
|
||||||
): Promise<RedmineTypes.Issue[]> {
|
|
||||||
const flatStore = new FlatIssuesStore();
|
|
||||||
const loader = this.createDynamicIssuesLoader();
|
|
||||||
if (query.query && query.query.selector) {
|
|
||||||
const issues = await this.find(query.query);
|
|
||||||
issues.forEach((issue) => flatStore.push(issue));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootIssues = [];
|
|
||||||
if (query.rootIds) {
|
|
||||||
const issues = await this.getIssues(query.rootIds);
|
|
||||||
rootIssues.push(...issues);
|
|
||||||
}
|
|
||||||
if (query.rootIssues) {
|
|
||||||
rootIssues.push(...query.rootIssues);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < rootIssues.length; i++) {
|
|
||||||
const rootIssue = rootIssues[i];
|
|
||||||
const issues = await this.getIssuesFromRoot(rootIssue);
|
|
||||||
issues.forEach((issue) => flatStore.push(issue));
|
|
||||||
}
|
|
||||||
|
|
||||||
await flatStore.fillData(loader);
|
|
||||||
|
|
||||||
return flatStore.getIssues();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ export class RedmineDataLoader {
|
||||||
issueNumber: number,
|
issueNumber: number,
|
||||||
skipEnhancers = false,
|
skipEnhancers = false,
|
||||||
): Promise<RedmineTypes.Issue | null> {
|
): Promise<RedmineTypes.Issue | null> {
|
||||||
if (issueNumber <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const url = this.getIssueUrl(issueNumber);
|
const url = this.getIssueUrl(issueNumber);
|
||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -46,32 +46,6 @@ export class UsersService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findUserByLogin(login: string): Promise<RedmineTypes.User | null> {
|
|
||||||
const userFromMemoryCache = this.memoryCache.find((item) => {
|
|
||||||
const email = item.mail;
|
|
||||||
if (!email) return false;
|
|
||||||
return email.startsWith(login);
|
|
||||||
});
|
|
||||||
if (userFromMemoryCache) {
|
|
||||||
return RedmineTypes.CreateUser(userFromMemoryCache);
|
|
||||||
}
|
|
||||||
const usersDb = await this.users.getDatasource();
|
|
||||||
const res = await usersDb.find({
|
|
||||||
selector: {
|
|
||||||
mail: {
|
|
||||||
$regex: login,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 1,
|
|
||||||
});
|
|
||||||
if (!res || !res.docs || !res.docs[0]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const userFromDb = res.docs[0];
|
|
||||||
this.memoryCache.set(userFromDb.id, userFromDb);
|
|
||||||
return RedmineTypes.CreateUser(userFromDb);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findUserByName(
|
async findUserByName(
|
||||||
firstname: string,
|
firstname: string,
|
||||||
lastname: string,
|
lastname: string,
|
||||||
|
|
|
||||||
2269
package-lock.json
generated
|
|
@ -36,17 +36,15 @@
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"imap-simple": "^5.1.0",
|
"imap-simple": "^5.1.0",
|
||||||
"jsonc-parser": "^3.2.0",
|
"jsonc-parser": "^3.2.0",
|
||||||
"jsonpath-plus": "^8.1.0",
|
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.1.0",
|
||||||
"moo": "^0.5.2",
|
"moo": "^0.5.2",
|
||||||
"nano": "^10.0.0",
|
"nano": "^10.0.0",
|
||||||
"node-telegram-bot-api": "^0.66.0",
|
"node-telegram-bot-api": "^0.59.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rss-parser": "^3.12.0",
|
"rss-parser": "^3.12.0",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.4.1"
|
||||||
"working-time-calculator": "^0.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.0.0",
|
"@nestjs/cli": "^8.0.0",
|
||||||
|
|
@ -56,11 +54,10 @@
|
||||||
"@types/cron": "^2.0.0",
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/jsonpath": "^0.2.4",
|
|
||||||
"@types/luxon": "^3.1.0",
|
"@types/luxon": "^3.1.0",
|
||||||
"@types/moo": "^0.5.6",
|
"@types/moo": "^0.5.6",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/node-telegram-bot-api": "^0.64.6",
|
"@types/node-telegram-bot-api": "^0.57.1",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.ser
|
||||||
import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer';
|
import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer';
|
||||||
import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards';
|
import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards';
|
||||||
import { DashboardInitService } from './dashboards/dashboard-init.service';
|
import { DashboardInitService } from './dashboards/dashboard-init.service';
|
||||||
import { TelegramBotController } from './telegram-bot/telegram-bot.controller';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -77,7 +76,6 @@ import { TelegramBotController } from './telegram-bot/telegram-bot.controller';
|
||||||
SimpleKanbanBoardController,
|
SimpleKanbanBoardController,
|
||||||
SimpleIssuesListController,
|
SimpleIssuesListController,
|
||||||
TagsManagerController,
|
TagsManagerController,
|
||||||
TelegramBotController,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { PersonalParsedMessage } from 'src/models/personal-parsed-message.model'
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonalNotificationsService {
|
export class PersonalNotificationsService {
|
||||||
private userNameRe = /@([\wА-Яа-яЁё]+) ([\wА-Яа-яЁё]+)@/g;
|
private userNameRe = /@([\wА-Яа-яЁё]+) ([\wА-Яа-яЁё]+)@/g;
|
||||||
private userName2Re = /@([\wА-Яа-яЁё\.]+)/g;
|
|
||||||
private logger = new Logger(PersonalNotificationsService.name);
|
private logger = new Logger(PersonalNotificationsService.name);
|
||||||
|
|
||||||
$messages = new Subject<IssueAndPersonalParsedMessageModel>();
|
$messages = new Subject<IssueAndPersonalParsedMessageModel>();
|
||||||
|
|
@ -72,16 +71,6 @@ export class PersonalNotificationsService {
|
||||||
}
|
}
|
||||||
result = results.next();
|
result = results.next();
|
||||||
}
|
}
|
||||||
const results2 = notes.matchAll(this.userName2Re);
|
|
||||||
let result2 = results2.next();
|
|
||||||
while (!result2.done) {
|
|
||||||
if (result.value && result.value[1]) {
|
|
||||||
const login = result.value[1];
|
|
||||||
const user = await this.usersService.findUserByLogin(login);
|
|
||||||
if (user) recipients.push(user.id);
|
|
||||||
}
|
|
||||||
result2 = results2.next();
|
|
||||||
}
|
|
||||||
if (recipients.length > 0) {
|
if (recipients.length > 0) {
|
||||||
return {
|
return {
|
||||||
message: notes,
|
message: notes,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Body, Controller, Post } from '@nestjs/common';
|
|
||||||
import { SendMessageParams, TelegramBotService } from './telegram-bot.service';
|
|
||||||
import {
|
|
||||||
BadRequestErrorHandler,
|
|
||||||
getOrAppErrorOrThrow,
|
|
||||||
} from '@app/event-emitter/utils/result';
|
|
||||||
|
|
||||||
@Controller('/api/telegram-bot')
|
|
||||||
export class TelegramBotController {
|
|
||||||
constructor(private telegramBotService: TelegramBotService) {}
|
|
||||||
|
|
||||||
@Post('send-message')
|
|
||||||
async sendMessage(@Body() params: SendMessageParams): Promise<void> {
|
|
||||||
await getOrAppErrorOrThrow(async () => {
|
|
||||||
await this.telegramBotService.sendMessageByParams(params);
|
|
||||||
}, BadRequestErrorHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,10 +8,6 @@ import { UserMetaInfoModel } from 'src/models/user-meta-info.model';
|
||||||
import { CurrentIssuesEccmBotHandlerService } from './handlers/current-issues-eccm.bot-handler.service';
|
import { CurrentIssuesEccmBotHandlerService } from './handlers/current-issues-eccm.bot-handler.service';
|
||||||
import { TelegramBotHandlerInterface } from './telegram.bot-handler.interface';
|
import { TelegramBotHandlerInterface } from './telegram.bot-handler.interface';
|
||||||
import { SetDailyEccmUserCommentBotHandlerService } from './handlers/set-daily-eccm-user-comment.bot-handler.service';
|
import { SetDailyEccmUserCommentBotHandlerService } from './handlers/set-daily-eccm-user-comment.bot-handler.service';
|
||||||
import { IssuesService } from '@app/event-emitter/issues/issues.service';
|
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
|
||||||
import { RedmineTypes } from '@app/event-emitter/models/redmine-types';
|
|
||||||
import { createAppError } from '@app/event-emitter/utils/result';
|
|
||||||
|
|
||||||
const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
|
||||||
|
|
||||||
|
|
@ -22,60 +18,6 @@ export function cutMessage(msg: string): string {
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OptionsParams = {
|
|
||||||
parse_mode?: TelegramBot.ParseMode | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecipientByName = {
|
|
||||||
firstname: string;
|
|
||||||
lastname: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecipientByIssueField = {
|
|
||||||
issueId: number;
|
|
||||||
field: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SendMessageParams = {
|
|
||||||
recipients: (RecipientByName | RecipientByIssueField)[];
|
|
||||||
msg: string;
|
|
||||||
options?: OptionsParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function validateSendMessageParams(params: SendMessageParams): string[] {
|
|
||||||
const res: string[] = [];
|
|
||||||
if (typeof params.msg !== 'string') {
|
|
||||||
res.push('Wrong msg field value, must be string');
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof params.recipients !== 'object' ||
|
|
||||||
typeof params.recipients.length !== 'number'
|
|
||||||
) {
|
|
||||||
res.push('Wrong recipients field value, must be array');
|
|
||||||
}
|
|
||||||
for (let i = 0; i < params.recipients?.length; i++) {
|
|
||||||
const r: any = params.recipients[i];
|
|
||||||
const checkRecipient = Boolean(
|
|
||||||
(typeof r.firstname === 'string' && typeof r.lastname === 'string') ||
|
|
||||||
(typeof r.issueId === 'number' && typeof r.field === 'string'),
|
|
||||||
);
|
|
||||||
if (!checkRecipient) {
|
|
||||||
res.push(
|
|
||||||
'Wrong recipient item value, must be object with firstname+lastname or issueId+field',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
params?.options?.parse_mode &&
|
|
||||||
['Markdown', 'MarkdownV2', 'HTML'].indexOf(params.options.parse_mode) < 0
|
|
||||||
) {
|
|
||||||
res.push(
|
|
||||||
`Wrong options.parse_mode value, must be one of 'Markdown', 'MarkdownV2', 'HTML'`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TelegramBotService {
|
export class TelegramBotService {
|
||||||
private logger = new Logger(TelegramBotService.name);
|
private logger = new Logger(TelegramBotService.name);
|
||||||
|
|
@ -93,7 +35,6 @@ export class TelegramBotService {
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private currentIssuesBotHandlerService: CurrentIssuesEccmBotHandlerService,
|
private currentIssuesBotHandlerService: CurrentIssuesEccmBotHandlerService,
|
||||||
private setDailyEccmUserCommentBotHandlerService: SetDailyEccmUserCommentBotHandlerService,
|
private setDailyEccmUserCommentBotHandlerService: SetDailyEccmUserCommentBotHandlerService,
|
||||||
private issuesService: IssuesService,
|
|
||||||
) {
|
) {
|
||||||
this.telegramBotToken = this.configService.get<string>('telegramBotToken');
|
this.telegramBotToken = this.configService.get<string>('telegramBotToken');
|
||||||
this.redminePublicUrlPrefix =
|
this.redminePublicUrlPrefix =
|
||||||
|
|
@ -179,18 +120,7 @@ export class TelegramBotService {
|
||||||
);
|
);
|
||||||
if (!userMetaInfo) return false;
|
if (!userMetaInfo) return false;
|
||||||
const chatId = userMetaInfo.telegram_chat_id;
|
const chatId = userMetaInfo.telegram_chat_id;
|
||||||
const formattedMsg = cutMessage(msg);
|
await this.bot.sendMessage(chatId, cutMessage(msg), options);
|
||||||
try {
|
|
||||||
await this.bot.sendMessage(chatId, formattedMsg, options);
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error at send message to telegram: ` +
|
|
||||||
`redmineId - ${redmineId}; ` +
|
|
||||||
`message - ${formattedMsg}; ` +
|
|
||||||
`exception - ${ex?.message || '<null message>'}`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Sent message for redmineUserId = ${redmineId}, ` +
|
`Sent message for redmineUserId = ${redmineId}, ` +
|
||||||
`telegramChatId = ${chatId}, ` +
|
`telegramChatId = ${chatId}, ` +
|
||||||
|
|
@ -212,62 +142,6 @@ export class TelegramBotService {
|
||||||
return await this.sendMessageByRedmineId(user.id, msg, options);
|
return await this.sendMessageByRedmineId(user.id, msg, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessageByParams(params: SendMessageParams): Promise<void> {
|
|
||||||
const paramsErrors = validateSendMessageParams(params);
|
|
||||||
if (paramsErrors && paramsErrors.length > 0) {
|
|
||||||
throw createAppError(`Params errors - ${JSON.stringify(paramsErrors)}`);
|
|
||||||
}
|
|
||||||
const issuesStore = await this.getIssuesStore(params);
|
|
||||||
for (let i = 0; i < params.recipients.length; i++) {
|
|
||||||
const recipient: any = params.recipients[i];
|
|
||||||
if (recipient.firstname && recipient.lastname) {
|
|
||||||
await this.sendMessageByName(
|
|
||||||
recipient.firstname,
|
|
||||||
recipient.lastname,
|
|
||||||
params.msg,
|
|
||||||
params.options,
|
|
||||||
);
|
|
||||||
} else if (recipient.issueId && recipient.field) {
|
|
||||||
const issue = issuesStore[recipient.issueId];
|
|
||||||
if (!issue) continue;
|
|
||||||
let fieldValue: any;
|
|
||||||
try {
|
|
||||||
fieldValue = JSONPath({ json: issue, path: recipient.field });
|
|
||||||
} catch (ex) {
|
|
||||||
const warnMsg =
|
|
||||||
`Error at get value from issueId = ${recipient.issueId}; ` +
|
|
||||||
`field = ${JSON.stringify(recipient.field)} ` +
|
|
||||||
`- ${ex.message}`;
|
|
||||||
this.logger.warn(warnMsg);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (fieldValue && fieldValue.length > 0) {
|
|
||||||
fieldValue = fieldValue[0];
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof fieldValue === 'object' &&
|
|
||||||
fieldValue.firstname &&
|
|
||||||
fieldValue.lastname
|
|
||||||
) {
|
|
||||||
await this.sendMessageByName(
|
|
||||||
fieldValue.firstname,
|
|
||||||
fieldValue.lastname,
|
|
||||||
params.msg,
|
|
||||||
params.options,
|
|
||||||
);
|
|
||||||
} else if (typeof fieldValue === 'number') {
|
|
||||||
await this.sendMessageByRedmineId(
|
|
||||||
fieldValue,
|
|
||||||
params.msg,
|
|
||||||
params.options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async register(
|
private async register(
|
||||||
msg: TelegramBot.Message,
|
msg: TelegramBot.Message,
|
||||||
): Promise<{ result: boolean; message: string }> {
|
): Promise<{ result: boolean; message: string }> {
|
||||||
|
|
@ -279,15 +153,7 @@ export class TelegramBotService {
|
||||||
`with message ${message}, ` +
|
`with message ${message}, ` +
|
||||||
`log data = ${JSON.stringify(logData || null)}`;
|
`log data = ${JSON.stringify(logData || null)}`;
|
||||||
this.logger.log(logMsg);
|
this.logger.log(logMsg);
|
||||||
try {
|
this.bot.sendMessage(msg.chat.id, message);
|
||||||
this.bot.sendMessage(msg.chat.id, message);
|
|
||||||
} catch (ex) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error at send message to telegram: ` +
|
|
||||||
`message - ${message}; ` +
|
|
||||||
`exception - ${ex?.message || '<null message>'}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { result: result, message: message };
|
return { result: result, message: message };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -336,24 +202,4 @@ export class TelegramBotService {
|
||||||
await this.userMetaInfoService.delete(userMetaInfo);
|
await this.userMetaInfoService.delete(userMetaInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getIssuesStore(
|
|
||||||
params: SendMessageParams,
|
|
||||||
): Promise<Record<number, RedmineTypes.Issue>> {
|
|
||||||
const issueIds: number[] = [];
|
|
||||||
for (let i = 0; i < params.recipients.length; i++) {
|
|
||||||
const recipient: any = params.recipients[i];
|
|
||||||
if (typeof recipient.issueId === 'number' && recipient.issueId > 0) {
|
|
||||||
issueIds.push(recipient.issueId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (issueIds.length <= 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const issues = await this.issuesService.getIssues(issueIds);
|
|
||||||
return issues.reduce((acc, issue) => {
|
|
||||||
acc[issue.id] = issue;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||