Skip to content

Commit 249205d

Browse files
authored
Merge pull request #422 from JohT/feature/markdown-table-support
Markdown table format support for Cypher queries
2 parents e8290c6 + 00351a4 commit 249205d

12 files changed

+405
-20
lines changed

scripts/executeQuery.sh

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,18 @@
1414
# -> "--no_source_reference" to not append the cypher query file name as last CSV column
1515
# -> any following key=value arguments are used as query parameters
1616

17+
# Requires markdown/formatQueryResultAsMarkdownTable.sh
18+
1719
# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands)
1820
set -o errexit -o pipefail
1921

22+
## Get this "scripts" directory if not already set
23+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
24+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
25+
# This way non-standard tools like readlink aren't needed.
26+
SCRIPTS_DIR=${SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts
27+
#echo "executeQuery: SCRIPTS_DIR=$SCRIPTS_DIR" >&2
28+
2029
# Overrideable Defaults
2130
NEO4J_HTTP_PORT=${NEO4J_HTTP_PORT:-"7474"} # Neo4j HTTP API port for executing queries
2231
NEO4J_HTTP_TRANSACTION_ENDPOINT=${NEO4J_HTTP_TRANSACTION_ENDPOINT:-"db/neo4j/tx/commit"} # Since Neo4j v5: "db/<name>/tx/commit", Neo4j v4: "db/data/transaction/commit"
@@ -35,29 +44,34 @@ fi
3544
cypher_query_file_name=""
3645
no_source_reference=false
3746
omit_query_error_highlighting=false
47+
output_markdown_table=false
3848
query_parameters=""
3949

4050
# Input Arguments: Function to print usage information
4151
print_usage() {
42-
echo "executeQuery Usage: $0 <filename> [--no-source-reference-column] [--omit-query-error-highlighting]" >&2
52+
echo "executeQuery Usage: $0 <filename> [--no-source-reference-column] [--omit-query-error-highlighting] [--output-markdown-table]" >&2
4353
echo "Options:" >&2
4454
echo " --no-source-reference-column: Exclude the source reference column" >&2
4555
echo " --omit-query-error-highlighting: Log query errors in same color as infos" >&2
56+
echo " --output-markdown-table: Output the result as markdown table instead of CSV" >&2
4657
}
4758

4859
# Input Arguments: Parse the command-line arguments
4960
while [[ $# -gt 0 ]]; do
5061
arg="$1"
51-
5262
case $arg in
5363
--no-source-reference-column)
5464
no_source_reference=true
5565
shift
5666
;;
57-
--omit_query_error_highlighting)
67+
--omit-query-error-highlighting)
5868
omit_query_error_highlighting=true
5969
shift
6070
;;
71+
--output-markdown-table)
72+
output_markdown_table=true
73+
shift
74+
;;
6175
*)
6276
if [[ -z "${cypher_query_file_name}" ]]; then
6377
# Input Arguments: Read the first unnamed input argument containing the name of the cypher file
@@ -134,11 +148,16 @@ if [[ -n "${error_message}" ]]; then
134148
exit 1
135149
fi
136150

137-
# Output results in CSV format
138-
if [ "${no_source_reference}" = true ] ; then
139-
echo -n "${cypher_query_result}" | jq -r '(.results[0])? | .columns,(.data[].row)? | map(if type == "array" then join(",") else . end) | flatten | @csv'
151+
if [ "${output_markdown_table}" = "true" ] ; then
152+
echo "executeQuery: Will output in Markdown Table Format" >&2
153+
echo -n "${cypher_query_result}" | "${SCRIPTS_DIR}/markdown/formatQueryResultAsMarkdownTable.sh"
140154
else
141-
cypher_query_file_relative_name=${cypher_query_file_name#/**/cypher/}
142-
sourceFileReferenceInfo="Source Cypher File: ${cypher_query_file_relative_name}"
143-
echo -n "${cypher_query_result}" | jq -r --arg sourceReference "${sourceFileReferenceInfo}" '(.results[0])? | .columns + [$sourceReference], (.data[].row)? + [""] | map(if type == "array" then join(",") else . end) | flatten | @csv'
155+
# Output results in CSV format
156+
if [ "${no_source_reference}" = true ] ; then
157+
echo -n "${cypher_query_result}" | jq -r '(.results[0])? | .columns,(.data[].row)? | map(if type == "array" then join(",") else . end) | flatten | @csv'
158+
else
159+
cypher_query_file_relative_name=${cypher_query_file_name#/**/cypher/}
160+
sourceFileReferenceInfo="Source Cypher File: ${cypher_query_file_relative_name}"
161+
echo -n "${cypher_query_result}" | jq -r --arg sourceReference "${sourceFileReferenceInfo}" '(.results[0])? | .columns + [$sourceReference], (.data[].row)? + [""] | map(if type == "array" then join(",") else . end) | flatten | @csv'
162+
fi
144163
fi
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env bash
2+
3+
# Processes a template_markdown_file markdown file, replacing placeholders like "<!-- include:intro.md -->" with the contents of the specified markdown files. The files to include needs to be in the "includes" subdirectory.
4+
5+
# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands)
6+
set -o errexit -o pipefail
7+
8+
## Get this "scripts" directory if not already set
9+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
10+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
11+
# This way non-standard tools like readlink aren't needed.
12+
MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts
13+
#echo "embedMarkdownIncludes: MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR}" >&2
14+
15+
template_markdown_file="$1"
16+
include_directory="includes"
17+
18+
awk -v include_directory="${include_directory}" '
19+
# Check if the filename is safe
20+
function is_safe(path) {
21+
if (substr(path, 1, 1) == "/") return 0
22+
if (path ~ /\.\./) return 0
23+
return 1
24+
}
25+
26+
function include_file(path, fullpath, line) {
27+
fullpath = include_directory "/" path
28+
29+
if (!is_safe(path)) {
30+
print "ERROR: illegal include path: " path > "/dev/stderr"
31+
exit 1
32+
}
33+
34+
if ((getline test < fullpath) < 0) {
35+
print "ERROR: missing file " fullpath > "/dev/stderr"
36+
exit 1
37+
}
38+
close(fullpath)
39+
40+
while ((getline line < fullpath) > 0) {
41+
print line
42+
}
43+
close(fullpath)
44+
}
45+
46+
{
47+
# Look for the include marker using index+substr (portable)
48+
if ($0 ~ /<!-- include:/) {
49+
start = index($0, "include:") + 8
50+
end = index($0, "-->")
51+
fname = substr($0, start, end - start)
52+
gsub(/^[ \t]+|[ \t]+$/, "", fname) # trim spaces
53+
54+
include_file(fname)
55+
} else {
56+
print
57+
}
58+
}
59+
' "${template_markdown_file}"
60+
61+
#echo "embedMarkdownIncludes: $(date +'%Y-%m-%dT%H:%M:%S%z') Successfully finished." >&2
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
3+
# Takes the input stream (Cypher query result in JSON format) and formats it as a Markdown table.
4+
5+
# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands)
6+
set -o errexit -o pipefail
7+
8+
## Get this "scripts" directory if not already set
9+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
10+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
11+
# This way non-standard tools like readlink aren't needed.
12+
MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts
13+
#echo "formatQueryResultAsMarkdownTable: MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR}" >&2
14+
15+
echo "formatQueryResultAsMarkdownTable: Will output in Markdown Table Format" >&2
16+
17+
# Read all input (including multiline) into cypher_query_result
18+
cypher_query_result=$(cat)
19+
20+
echo -n "${cypher_query_result}" | jq -r '
21+
# Take the first query result
22+
.results[0] as $result
23+
24+
# Extract the column names
25+
| $result.columns as $columns
26+
27+
# Build the Markdown header row
28+
| ( "| " + ( $columns | join(" | ") ) + " |" )
29+
30+
# Build the Markdown separator row
31+
, ( "| " + ( $columns | map("---") | join(" | ") ) + " |" )
32+
33+
# Build one row for each data entry
34+
, ( $result.data[].row
35+
| map(tostring)
36+
| "| " + ( join(" | ") ) + " |"
37+
)
38+
'
39+
40+
#echo "formatQueryResultAsMarkdownTable: $(date +'%Y-%m-%dT%H:%M:%S%z') Successfully finished." >&2
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env bash
2+
3+
# Tests template processing for markdown by embedding includes.
4+
5+
# Requires embedMarkdownIncludes.sh
6+
7+
# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands)
8+
set -o errexit -o pipefail
9+
10+
## Get this "scripts" directory if not already set
11+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
12+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
13+
# This way non-standard tools like readlink aren't needed.
14+
MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts
15+
echo "testEmbedMarkdownIncludes: MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR}" >&2
16+
17+
tearDown() {
18+
# echo "testEmbedMarkdownIncludes: Tear down tests...."
19+
rm -rf "${temporaryTestDirectory}"
20+
}
21+
22+
successful() {
23+
local COLOR_SUCCESSFUL="\033[0;32m" # green
24+
local COLOR_DEFAULT='\033[0m'
25+
26+
echo -e "testEmbedMarkdownIncludes: ${COLOR_SUCCESSFUL}Tests finished successfully.${COLOR_DEFAULT}"
27+
28+
tearDown
29+
}
30+
31+
fail() {
32+
local COLOR_ERROR='\033[0;31m' # red
33+
local COLOR_DEFAULT='\033[0m'
34+
35+
local errorMessage="${1}"
36+
37+
echo -e "testEmbedMarkdownIncludes: ${COLOR_ERROR}${errorMessage}${COLOR_DEFAULT}"
38+
tearDown
39+
return 1
40+
}
41+
42+
echo "testEmbedMarkdownIncludes: Starting tests...."
43+
44+
# Create testing resources
45+
temporaryTestDirectory=$(mktemp -d 2>/dev/null || mktemp -d -t 'temporaryTestDirectory')
46+
47+
testMarkdownTemplate="${temporaryTestDirectory}/testMarkdownTemplate.md"
48+
echo "<!-- include:testInclude.md -->" > "${testMarkdownTemplate}"
49+
50+
# Setup test files
51+
mkdir -p "${temporaryTestDirectory}/includes"
52+
53+
# ------------------------------------------------------------
54+
# Test case --
55+
# ------------------------------------------------------------
56+
echo "testEmbedMarkdownIncludes: 1.) An existing include file is correctly embedded."
57+
58+
# - Setup
59+
testIncludeFile="includes/testInclude.md"
60+
expected_test_include_content="This is the included content for the test."
61+
echo "${expected_test_include_content}" > "${temporaryTestDirectory}/${testIncludeFile}"
62+
63+
# - Execute script under test
64+
embeddedContent=$(cd "${temporaryTestDirectory}"; "${MARKDOWN_SCRIPTS_DIR}/embedMarkdownIncludes.sh" "${testMarkdownTemplate}" )
65+
66+
# - Verify results
67+
if [ "${embeddedContent}" != "${expected_test_include_content}" ]; then
68+
fail "1.) Test failed: Expected embedded content to be '${expected_test_include_content}', but got '${embeddedContent}'."
69+
fi
70+
71+
# ------------------------------------------------------------
72+
# Test case --
73+
# ------------------------------------------------------------
74+
echo "testEmbedMarkdownIncludes: 2.) A missing include file results in an error."
75+
76+
# - Setup
77+
testMarkdownTemplateMissingInclude="testMarkdownTemplateMissingInclude.md"
78+
echo "<!-- include:nonExistentFile.md -->" > "${temporaryTestDirectory}/${testMarkdownTemplateMissingInclude}"
79+
80+
# - Execute script under test
81+
set +o errexit
82+
errorOutput=$(cd "${temporaryTestDirectory}"; { "${MARKDOWN_SCRIPTS_DIR}/embedMarkdownIncludes.sh" "${testMarkdownTemplateMissingInclude}" 2>&1 1>/dev/null; } )
83+
exitCode=$?
84+
set -o errexit
85+
86+
# - Verify results
87+
if [ ${exitCode} -eq 0 ]; then
88+
fail "2.) Test failed: Expected an error due to missing include file, but the script succeeded."
89+
fi
90+
if [[ "${errorOutput}" != *"ERROR: missing file"* ]]; then
91+
fail "2.) Test failed: Expected error message to contain 'ERROR: missing file', but got '${errorOutput}'."
92+
fi
93+
94+
successful
95+
return 0
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
3+
# Tests formatting of Cypher query results as Markdown table.
4+
5+
# Requires formatQueryResultAsMarkdownTable.sh
6+
7+
# Fail on any error ("-e" = exit on first error, "-o pipefail" exist on errors within piped commands)
8+
set -o errexit -o pipefail
9+
10+
## Get this "scripts" directory if not already set
11+
# Even if $BASH_SOURCE is made for Bourne-like shells it is also supported by others and therefore here the preferred solution.
12+
# CDPATH reduces the scope of the cd command to potentially prevent unintended directory changes.
13+
# This way non-standard tools like readlink aren't needed.
14+
MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts
15+
#echo "testFormatQueryResultAsMarkdownTable: MARKDOWN_SCRIPTS_DIR=${MARKDOWN_SCRIPTS_DIR}" >&2
16+
17+
tearDown() {
18+
# echo "testFormatQueryResultAsMarkdownTable: Tear down tests...."
19+
rm -rf "${temporaryTestDirectory}"
20+
}
21+
22+
successful() {
23+
local COLOR_SUCCESSFUL="\033[0;32m" # green
24+
local COLOR_DEFAULT='\033[0m'
25+
26+
echo -e "testFormatQueryResultAsMarkdownTable: ${COLOR_SUCCESSFUL}Tests finished successfully.${COLOR_DEFAULT}"
27+
28+
tearDown
29+
}
30+
31+
fail() {
32+
local COLOR_ERROR='\033[0;31m' # red
33+
local COLOR_DEFAULT='\033[0m'
34+
35+
local errorMessage="${1}"
36+
37+
echo -e "testFormatQueryResultAsMarkdownTable: ${COLOR_ERROR}${errorMessage}${COLOR_DEFAULT}"
38+
tearDown
39+
return 1
40+
}
41+
42+
echo "testFormatQueryResultAsMarkdownTable: Starting tests...."
43+
44+
# Create testing resources
45+
temporaryTestDirectory=$(mktemp -d 2>/dev/null || mktemp -d -t 'temporaryTestDirectory')
46+
47+
# ------------------------------------------------------------
48+
# Test case --
49+
# ------------------------------------------------------------
50+
echo "testFormatQueryResultAsMarkdownTable: 1.) Convert a simple query result to a Markdown table."
51+
52+
# Read expected result from test_data_cypher_query_result_simple_expected
53+
expected_result=$(<"${MARKDOWN_SCRIPTS_DIR}/test_data_cypher_query_result_simple_expected.md")
54+
55+
# - Execute script under test
56+
embeddedContent=$(cd "${temporaryTestDirectory}"; cat "${MARKDOWN_SCRIPTS_DIR}/test_data_cypher_query_result_simple.json" | "${MARKDOWN_SCRIPTS_DIR}/formatQueryResultAsMarkdownTable.sh")
57+
58+
# - Verify results
59+
if [ "${embeddedContent}" != "${expected_result}" ]; then
60+
fail "1.) Test failed: Expected Markdown table to be \n${expected_result}, but got:\n${embeddedContent}"
61+
fi
62+
63+
64+
echo "testFormatQueryResultAsMarkdownTable: 2.) Convert an array query result to a Markdown table."
65+
66+
# Read expected result from test_data_cypher_query_result_simple_expected
67+
expected_result=$(<"${MARKDOWN_SCRIPTS_DIR}/test_data_cypher_query_result_with_array_expected.md")
68+
69+
# - Execute script under test
70+
embeddedContent=$(cd "${temporaryTestDirectory}"; cat "${MARKDOWN_SCRIPTS_DIR}/test_data_cypher_query_result_with_array.json" | "${MARKDOWN_SCRIPTS_DIR}/formatQueryResultAsMarkdownTable.sh")
71+
72+
# - Verify results
73+
if [ "${embeddedContent}" != "${expected_result}" ]; then
74+
fail "2.) Test failed: Expected Markdown table to be \n${expected_result}, but got:\n${embeddedContent}"
75+
fi
76+
77+
successful
78+
return 0
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"results": [
3+
{
4+
"columns": [
5+
"nodeLabel",
6+
"nodesWithThatLabel",
7+
"nodesWithThatLabelPercent"
8+
],
9+
"data": [
10+
{
11+
"row": [
12+
"Git",
13+
234151,
14+
77.73524646765112
15+
]
16+
},
17+
{
18+
"row": [
19+
"Change",
20+
213807,
21+
70.98128917454584
22+
]
23+
},
24+
{
25+
"row": [
26+
"Update",
27+
138781,
28+
46.073581748645495
29+
]
30+
}
31+
]
32+
}
33+
],
34+
"errors": []
35+
}

0 commit comments

Comments
 (0)