Skip to content

Commit 837e944

Browse files
authored
Add basic auth and timeout support to the CLI
* Support basic auth in CLI * Use GitHub CI service containers for CLI test * Remove positional parameters, add timeout option * Update CI for new CLI usage * Run CLI test for all tested Node versions * Fix the CLI to use the correct endpoint arument * Better naming for the CLI test steps * Update readme for new CLI commands, fix help usage * Add CLI test for ASK queries * Fix endpoint URL in ASK test * Use environment variables for basic auth * Add bash script for CLI tests in CI * Use docker container create and start, not run * Remove suffix from name, remove container after use * Separate CLI test to its own job * Remove trailing whitespace * Print detailed errors in CLI * Set default timeout to 10 seconds * Remove unnecessary github.workspace use from CI * Use jena_port variable, remove surplus slashes * Revert to old-style CI scripts * Update CLI use in test script * One more test with the new script * Try separating Docker pull and create * Try running the bash script for every Node version --------- Co-authored-by: surilindur <surilindur@users.noreply.github.com>
1 parent 38772d4 commit 837e944

File tree

8 files changed

+124
-128
lines changed

8 files changed

+124
-128
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ concurrency:
88
group: ${{ github.workflow }}-${{ github.ref }}
99
cancel-in-progress: true
1010

11+
env:
12+
DEFAULT_NODE_VERSION: 20.x
13+
1114
jobs:
1215

1316
lint:
@@ -20,7 +23,7 @@ jobs:
2023
key: ${{ runner.os }}-lint-modules-${{ hashFiles('**/yarn.lock') }}
2124
- uses: actions/setup-node@v4
2225
with:
23-
node-version: 20.x
26+
node-version: ${{ env.DEFAULT_NODE_VERSION }}
2427
- run: corepack enable
2528
- run: yarn install --ignore-engines --frozen-lockfile
2629
- run: yarn run lint
@@ -46,6 +49,8 @@ jobs:
4649
with:
4750
path: '**/node_modules'
4851
key: ${{ runner.os }}-test-modules-${{ hashFiles('**/yarn.lock') }}
52+
- name: Start Jena SPARQL endpoint
53+
run: .github/workflows/jena.sh start
4954
- run: corepack enable
5055
- run: yarn install --ignore-engines --frozen-lockfile
5156
- run: yarn run build
@@ -55,6 +60,11 @@ jobs:
5560
github-token: ${{ secrets.github_token }}
5661
flag-name: run-${{ matrix.node-version }}
5762
parallel: true
63+
- name: Test the command line tool against Jena SPARQL endpoint
64+
run: .github/workflows/jena.sh test
65+
- name: Stop Jena SPARQL endpoint
66+
run: .github/workflows/jena.sh stop
67+
if: always()
5868

5969
coveralls:
6070
needs: test
@@ -66,26 +76,6 @@ jobs:
6676
github-token: ${{ secrets.github_token }}
6777
parallel-finished: true
6878

69-
cli:
70-
needs: test
71-
runs-on: ubuntu-latest
72-
steps:
73-
- uses: actions/setup-node@v4
74-
with:
75-
node-version: 20.x
76-
- run: git config --global core.autocrlf input
77-
- uses: actions/checkout@v4
78-
- run: .github/workflows/start-docker.sh
79-
- uses: actions/cache@v4
80-
with:
81-
path: '**/node_modules'
82-
key: ${{ runner.os }}-docker-modules-${{ hashFiles('**/yarn.lock') }}
83-
- run: corepack enable
84-
- run: yarn install --ignore-engines --frozen-lockfile
85-
- run: yarn build
86-
- run: .github/workflows/test-docker.sh
87-
- run: .github/workflows/stop-docker.sh
88-
8979
webpack:
9080
needs: test
9181
runs-on: ubuntu-latest
@@ -97,7 +87,7 @@ jobs:
9787
key: ${{ runner.os }}-webpack-modules-${{ hashFiles('**/yarn.lock') }}
9888
- uses: actions/setup-node@v4
9989
with:
100-
node-version: 20.x
90+
node-version: ${{ env.DEFAULT_NODE_VERSION }}
10191
- run: corepack enable
10292
- run: yarn install --ignore-engines --frozen-lockfile
10393
- run: npx webpack

.github/workflows/jena.sh

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/bin/bash
2+
3+
jena_port=4000
4+
jena_name=endpoint
5+
jena_image=stain/jena-fuseki:4.8.0
6+
jena_dataset=mydataset
7+
jena_password=pw
8+
jena_username=admin
9+
cli_timeout=10
10+
11+
if [ "$1" = "start" ]; then
12+
echo "Starting Jena container with name $jena_name using $jena_image"
13+
docker pull "$jena_image"
14+
docker container create \
15+
--name "$jena_name" \
16+
--publish "$jena_port":3030 \
17+
--env FUSEKI_DATASET_1="$jena_dataset" \
18+
--env ADMIN_PASSWORD="$jena_password" \
19+
"$jena_image"
20+
docker start "$jena_name"
21+
22+
elif [ "$1" = "stop" ]; then
23+
echo "Stopping Jena container with name $jena_name"
24+
docker stop "$jena_name"
25+
docker container remove "$jena_name"
26+
27+
elif [ "$1" = "test" ]; then
28+
echo "Testing the command line tool against the Jena SPARQL endpoint with basic auth..."
29+
30+
result=$( \
31+
SPARQL_USERNAME="$jena_username" SPARQL_PASSWORD="$jena_password" node bin/fetch-sparql-endpoint.js \
32+
--endpoint "http://localhost:$jena_port/$jena_dataset/update" \
33+
--query 'insert data { <ex:s> <ex:p> <ex:o> }' \
34+
--auth basic \
35+
--timeout "$cli_timeout" \
36+
)
37+
echo "$result" | grep 'OK' &> /dev/null && echo "SPARQL UPDATE OK" || (echo "SPARQL UPDATE failed: $result" && exit 1)
38+
39+
result=$( \
40+
SPARQL_USERNAME="$jena_username" SPARQL_PASSWORD="$jena_password" node bin/fetch-sparql-endpoint.js \
41+
--endpoint "http://localhost:$jena_port/$jena_dataset/sparql" \
42+
--query 'ask where { <ex:s> <ex:p> <ex:o> }' \
43+
--auth basic \
44+
--timeout "$cli_timeout" \
45+
)
46+
echo "$result" | grep 'true' &> /dev/null && echo "SPARQL ASK OK" || (echo "SPARQL ASK failed: $result" && exit 1)
47+
48+
result=$( \
49+
SPARQL_USERNAME="$jena_username" SPARQL_PASSWORD="$jena_password" node bin/fetch-sparql-endpoint.js \
50+
--endpoint "http://localhost:$jena_port/$jena_dataset/sparql" \
51+
--query 'select * where { ?s ?p ?o }' \
52+
--auth basic \
53+
--timeout "$cli_timeout" \
54+
)
55+
echo "$result" | grep '{"s":"ex:s","p":"ex:p","o":"ex:o"}' &> /dev/null && echo "SPARQL SELECT OK" || (echo "SPARQL SELECT failed: $result" && exit 1)
56+
fi

.github/workflows/shiro.ini

Lines changed: 0 additions & 55 deletions
This file was deleted.

.github/workflows/start-docker.sh

Lines changed: 0 additions & 8 deletions
This file was deleted.

.github/workflows/stop-docker.sh

Lines changed: 0 additions & 2 deletions
This file was deleted.

.github/workflows/test-docker.sh

Lines changed: 0 additions & 11 deletions
This file was deleted.

README.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,31 @@ This method will also throw an error if the query contains a syntax error.
141141

142142
### Command-line
143143

144-
A command-line tool is provided to quickly query or update any SPARQL endpoint:
144+
A command-line tool is provided to quickly query or update any SPARQL endpoint.
145+
With basic authentication, the username and password should be made available
146+
via process-scoped environment variables `SPARQL_USERNAME` and `SPARQL_PASSWORD`.
145147

146148
Usage:
147149
```
148-
fetch-sparql-endpoint Sends a query to a SPARQL endpoint
149-
150-
Usage:
151-
fetch-sparql-endpoint https://dbpedia.org/sparql [-q] 'SELECT * WHERE { ?s ?p ?o } 100'
152-
fetch-sparql-endpoint https://dbpedia.org/sparql -f query.sparql
153-
cat query.sparql | fetch-sparql-endpoint https://dbpedia.org/sparql
154-
155150
Options:
156-
-q evaluate the given SPARQL query string
157-
-f evaluate the SPARQL query in the given file
158-
-g send query via HTTP GET instead of POST
159-
--help print this help message
151+
--endpoint Send the query to this SPARQL endpoint [string] [required]
152+
--query Evaluate the given SPARQL query string [string]
153+
--file Evaluate the SPARQL query in the given file [string]
154+
--get Send query via HTTP GET instead of POST [boolean] [default: false]
155+
--timeout The timeout value in seconds to finish the query [number]
156+
--auth The type of authentication to use [choices: "basic"]
157+
--version Show version number [boolean]
158+
--help Show help [boolean]
159+
160+
Examples:
161+
fetch-sparql-endpoint.js --endpoint Fetch 100 triples from the DBPedia
162+
https://dbpedia.org/sparql --query SPARQL endpoint
163+
'SELECT * WHERE { ?s ?p ?o } LIMIT 100'
164+
fetch-sparql-endpoint.js --endpoint Run the SPARQL query from query.rq
165+
https://dbpedia.org/sparql --file against the DBPedia SPARQL endpoint
166+
query.rq
167+
cat query.rq | fetch-sparql-endpoint.js Run the SPARQL query from query.rq
168+
--endpoint https://dbpedia.org/sparql against the DBPedia SPARQL endpoint
160169
```
161170

162171
## License

bin/fetch-sparql-endpoint.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,48 +64,65 @@ function update(endpoint: string, fetcher: SparqlEndpointFetcher, query: string)
6464
}
6565

6666
async function run(argv: string[]): Promise<void> {
67-
const args = await yargs(hideBin(argv))
67+
const args = await yargs(hideBin(argv), 'Sends a query to a SPARQL endpoint')
6868
.example(
69-
'$0 https://dbpedia.org/sparql --query \'SELECT * WHERE { ?s ?p ?o } LIMIT 100\'',
69+
'$0 --endpoint https://dbpedia.org/sparql --query \'SELECT * WHERE { ?s ?p ?o } LIMIT 100\'',
7070
'Fetch 100 triples from the DBPedia SPARQL endpoint',
7171
)
7272
.example(
73-
'$0 https://dbpedia.org/sparql --file query.rq',
73+
'$0 --endpoint https://dbpedia.org/sparql --file query.rq',
7474
'Run the SPARQL query from query.rq against the DBPedia SPARQL endpoint',
7575
)
7676
.example(
77-
'cat query.rq | $0 https://dbpedia.org/sparql',
77+
'cat query.rq | $0 --endpoint https://dbpedia.org/sparql',
7878
'Run the SPARQL query from query.rq against the DBPedia SPARQL endpoint',
7979
)
80-
.positional('endpoint', { type: 'string', describe: 'Send the query to this SPARQL endpoint', demandOption: true })
81-
.positional('query', { type: 'string', describe: 'The query to execute' })
8280
.options({
83-
query: { alias: 'q', type: 'string', describe: 'Evaluate the given SPARQL query string' },
84-
file: { alias: 'f', type: 'string', describe: 'Evaluate the SPARQL query in the given file' },
85-
get: { alias: 'g', type: 'boolean', describe: 'Send query via HTTP GET instead of POST', default: false },
81+
endpoint: { type: 'string', describe: 'Send the query to this SPARQL endpoint', demandOption: true },
82+
query: { type: 'string', describe: 'Evaluate the given SPARQL query string' },
83+
file: { type: 'string', describe: 'Evaluate the SPARQL query in the given file' },
84+
get: { type: 'boolean', describe: 'Send query via HTTP GET instead of POST', default: false },
85+
timeout: { type: 'number', describe: 'The timeout value in seconds to finish the query' },
86+
auth: { choices: [ 'basic' ], describe: 'The type of authentication to use' },
8687
})
87-
.help()
88+
.check((arg) => {
89+
if (arg.auth === 'basic' && (!process.env.SPARQL_USERNAME || !process.env.SPARQL_PASSWORD)) {
90+
throw new Error('Basic authentication requires the SPARQL_USERNAME and SPARQL_PASSWORD environment variables.');
91+
}
92+
return true;
93+
})
94+
.version()
95+
.help('help')
8896
.parse();
89-
const endpoint = <string>args._[0];
90-
const queryString = await getQuery(args._.length > 1 ? <string>args._[1] : args.query, args.file);
91-
const fetcher = new SparqlEndpointFetcher({ method: args.get ? 'GET' : 'POST' });
97+
const queryString = await getQuery(args.query, args.file);
98+
let defaultHeaders: Headers | undefined;
99+
if (args.auth === 'basic') {
100+
defaultHeaders = new Headers({
101+
authorization: `Basic ${Buffer.from(`${process.env.SPARQL_USERNAME}:${process.env.SPARQL_PASSWORD}`, 'binary').toString('base64')}`,
102+
});
103+
}
104+
const fetcher = new SparqlEndpointFetcher({
105+
method: args.get ? 'GET' : 'POST',
106+
timeout: args.timeout ? args.timeout * 1_000 : undefined,
107+
defaultHeaders,
108+
});
92109
const queryType = fetcher.getQueryType(queryString);
93110
switch (queryType) {
94111
case 'SELECT':
95-
await querySelect(endpoint, fetcher, queryString);
112+
await querySelect(args.endpoint, fetcher, queryString);
96113
break;
97114
case 'ASK':
98-
await queryAsk(endpoint, fetcher, queryString);
115+
await queryAsk(args.endpoint, fetcher, queryString);
99116
break;
100117
case 'CONSTRUCT':
101-
await queryConstruct(endpoint, fetcher, queryString);
118+
await queryConstruct(args.endpoint, fetcher, queryString);
102119
break;
103120
case 'UNKNOWN':
104121
if (fetcher.getUpdateTypes(queryString) !== 'UNKNOWN') {
105-
await update(endpoint, fetcher, queryString);
122+
await update(args.endpoint, fetcher, queryString);
106123
}
107124
break;
108125
}
109126
}
110127

111-
run(process.argv).then().catch(error => process.stderr.write(`${error.toString()}\n`));
128+
run(process.argv).then().catch((error: Error) => process.stderr.write(`${error.name}: ${error.message}\n${error.stack}`));

0 commit comments

Comments
 (0)