Skip to content

Commit 3a39bf2

Browse files
authored
Merge pull request #464 from yetanalytics/csv-export
[SQL-281] CSV export
2 parents 1f4268c + 35b11fa commit 3a39bf2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1242
-263
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Version of LRS Admin UI to use
44

5-
LRS_ADMIN_UI_VERSION ?= v0.2.0
5+
LRS_ADMIN_UI_VERSION ?= v0.2.2
66
LRS_ADMIN_UI_LOCATION ?= https://github.com/yetanalytics/lrs-admin-ui/releases/download/${LRS_ADMIN_UI_VERSION}/lrs-admin-ui.zip
77
LRS_ADMIN_ZIPFILE ?= lrs-admin-ui-${LRS_ADMIN_UI_VERSION}.zip
88

deps.edn

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
org.clojure/clojure {:mvn/version "1.11.2"}
55
org.clojure/tools.logging {:mvn/version "1.1.0"}
66
org.clojure/core.memoize {:mvn/version "1.0.250"}
7+
org.clojure/data.csv {:mvn/version "1.1.0"}
78
clojure-interop/java.security {:mvn/version "1.0.5"}
89
org.clojure/core.async {:mvn/version "1.6.681"}
910
;; Util deps
@@ -49,13 +50,11 @@
4950
less-awful-ssl/less-awful-ssl {:mvn/version "1.0.6"}
5051
xyz.capybara/clamav-client {:mvn/version "2.1.2"}
5152
;; Yet Analytics deps
52-
5353
com.yetanalytics/lrs
5454
{:mvn/version "1.3.0"
5555
:exclusions [org.clojure/clojure
5656
org.clojure/clojurescript
5757
com.yetanalytics/xapi-schema]}
58-
5958
com.yetanalytics/xapi-schema
6059
{:mvn/version "1.4.0"
6160
:exclusions [org.clojure/clojure
@@ -64,15 +63,15 @@
6463
{:mvn/version "0.1.4"
6564
:exclusions [org.clojure/clojure
6665
org.clojure/clojurescript]}
66+
com.yetanalytics/pathetic
67+
{:mvn/version "0.5.0"}
6768
com.yetanalytics/pedestal-oidc
6869
{:mvn/version "0.0.8"
6970
:exclusions [org.clojure/clojure
7071
buddy/buddy-sign]}
71-
7272
com.yetanalytics/lrs-reactions
7373
{:mvn/version "0.0.1"
7474
:exclusions [org.clojure/clojure]}
75-
7675
com.yetanalytics/gen-openapi
7776
{:mvn/version "0.0.4"
7877
:exclusions [org.clojure/clojure

doc/endpoints.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ The response body contains a newly generated JSON Web Token (JWT) on success. A
4242
- `GET http://example.org/admin/creds`: Read all credential pairs and their associated scopes for a particular account (denoted by the JWT).
4343
- `DELETE http://example.org/admin/creds`: Delete an existing credential pair, given by the `api-key` and `secret-key` properties in the request body, as well as any associated scopes.
4444

45+
#### Statement CSV Download
46+
47+
- `GET http://example.org/admin/csv/auth`: Return a one-time JWT for use for `/admin/csv`, used in order to use the latter endpoint as a `download`
48+
attribute for HTML anchor tags and authenticate without headers.
49+
- `GET http://example.org/admin/csv`: Download statements in the LRS as a CSV filestream. This endpoint accepts the statement query parameters defined in the [xAPI spec](https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#213-get-statements), but allows two additional parameters: the one-time JWT `token` string and the URL-encoded `property-path` vector strings.
50+
4551
#### Misc Admin Routes
4652

4753
- `GET http://example.org/admin/env`: Get select environment variables about the configuration which may aid in client-side operations.

doc/env_vars.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ The following options are used for advanced database performance tuning and may
101101
| `LRSQL_API_SECRET_DEFAULT` | `apiSecretDefault` | The secret API key that seeds the credential table. Optional, and is ignored if no seed admin account is set. | Not set |
102102
| `LRSQL_STMT_GET_DEFAULT` | `stmtGetDefault` | The default `limit` value in a statement query. Queries default to this value if not explicitly set. | `50` |
103103
| `LRSQL_STMT_GET_MAX` | `stmtGetMax` | The maximum allowed `limit` value for a statement query. If an explicit `limit` value exceeds this value, it will be overridden. | `50` |
104+
| `LRSQL_STMT_GET_MAX_CSV` | `stmtGetMaxCsv` | The maximum allowed `limit` value applied to a statement download. If an explicit `limit` value exceeds this value, it will be overridden. If negative (e.g. the default value of `-1`), it will be ignored and all statements in the LRS will be downloaded. | -1 |
104105
| `LRSQL_AUTHORITY_TEMPLATE` | `authorityTemplate` | The filepath to the Statement authority template file, which describes how authorities are constructed during statement insertion. If the file is not found, the system defaults to a default authority function. | <details>`config/authority.json.template`<summary>(Filepath)</summary></details> |
105106
| `LRSQL_AUTHORITY_URL` | `authorityUrl` | The URL that is set as the `authority-url` value when constructing an authority from a template. | `http://example.org` |
106107
| `LRSQL_OIDC_AUTHORITY_TEMPLATE` | `oidcAuthorityTemplate` | Like `LRSQL_AUTHORITY_TEMPLATE`, but only used when forming an authority from an OIDC access token. | <details>`config/oidc_authority.json.template`<summary>(Filepath)</summary></details> |

resources/lrsql/config/prod/default/lrs.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
:api-secret-default #or [#env LRSQL_API_SECRET_DEFAULT nil]
55
:stmt-get-default #long #or [#env LRSQL_STMT_GET_DEFAULT 50]
66
:stmt-get-max #long #or [#env LRSQL_STMT_GET_MAX 50]
7+
:stmt-get-max-csv #long #or [#env LRSQL_STMT_GET_MAX_CSV -1]
78
:stmt-url-prefix "/xapi" ; overriden by ::webserver/url-prefix
89
:authority-template #or [#env LRSQL_AUTHORITY_TEMPLATE "config/authority.json.template"]
910
:authority-url #or [#env LRSQL_AUTHORITY_URL "http://example.org"]

resources/lrsql/config/test/default/lrs.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
:api-secret-default "password"
55
:stmt-get-default 50
66
:stmt-get-max 50
7+
:stmt-get-max-csv nil
78
:stmt-url-prefix "/xapi"
89
:authority-template "config/authority.json.template"
910
:authority-url "http://example.org"

src/db/postgres/lrsql/postgres/record.clj

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns lrsql.postgres.record
22
(:require [com.stuartsierra.component :as cmp]
33
[hugsql.core :as hug]
4+
[next.jdbc :as jdbc]
45
[lrsql.backend.data :as bd]
56
[lrsql.backend.protocol :as bp]
67
[lrsql.init :refer [init-hugsql-adapter!]]
@@ -18,6 +19,8 @@
1819
(hug/def-db-fns "lrsql/postgres/sql/update.sql")
1920
(hug/def-db-fns "lrsql/postgres/sql/delete.sql")
2021

22+
(hug/def-sqlvec-fns "lrsql/postgres/sql/query.sql")
23+
2124
;; Define record
2225
#_{:clj-kondo/ignore [:unresolved-symbol]} ; Shut up VSCode warnings
2326
(defrecord PostgresBackend [tuning]
@@ -77,7 +80,8 @@
7780
(add-statement-to-actor-cascading-delete! tx))
7881
(when (some? (query-varchar-exists tx))
7982
(convert-varchars-to-text! tx))
80-
(create-blocked-jwt-table! tx))
83+
(create-blocked-jwt-table! tx)
84+
(alter-blocked-jwt-add-one-time-id! tx))
8185

8286
bp/BackendUtil
8387
(-txn-retry? [_ ex]
@@ -103,6 +107,12 @@
103107
(query-statement-exists tx input))
104108
(-query-statement-descendants [_ tx input]
105109
(query-statement-descendants tx input))
110+
(-query-statements-lazy [_ tx input]
111+
(let [sqlvec (query-statements-sqlvec input)]
112+
(jdbc/plan tx sqlvec {:fetch-size 4000
113+
:concurrency :read-only
114+
:cursors :close
115+
:result-type :forward-only})))
106116

107117
bp/ActorBackend
108118
(-insert-actor! [_ tx input]
@@ -201,10 +211,16 @@
201211
bp/JWTBlocklistBackend
202212
(-insert-blocked-jwt! [_ tx input]
203213
(insert-blocked-jwt! tx input))
214+
(-insert-one-time-jwt! [_ tx input]
215+
(insert-one-time-jwt! tx input))
216+
(-update-one-time-jwt! [_ tx input]
217+
(update-one-time-jwt! tx input))
204218
(-delete-blocked-jwt-by-time! [_ tx input]
205219
(delete-blocked-jwt-by-time! tx input))
206220
(-query-blocked-jwt [_ tx input]
207221
(query-blocked-jwt-exists tx input))
222+
(-query-one-time-jwt [_ tx input]
223+
(query-one-time-jwt-exists tx input))
208224

209225
bp/CredentialBackend
210226
(-insert-credential! [_ tx input]

src/db/postgres/lrsql/postgres/sql/ddl.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,10 @@ CREATE TABLE IF NOT EXISTS blocked_jwt (
499499
evict_time TIMESTAMP WITH TIME ZONE
500500
);
501501
CREATE INDEX IF NOT EXISTS blocked_jwt_evict_time_idx ON blocked_jwt(evict_time);
502+
503+
/* Migration 2025-03-05 - Add One-Time ID to Blocklist Table */
504+
505+
-- :name alter-blocked-jwt-add-one-time-id!
506+
-- :command :execute
507+
-- :doc Add the column `blocked_jwt.one_time_id` for one-time JWTs; JWTs with one-time IDs are not considered blocked yet.
508+
ALTER TABLE IF EXISTS blocked_jwt ADD COLUMN IF NOT EXISTS one_time_id UUID UNIQUE;

src/db/postgres/lrsql/postgres/sql/insert.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,13 @@ INSERT INTO blocked_jwt (
171171
) VALUES (
172172
:jwt, :eviction-time
173173
);
174+
175+
-- :name insert-one-time-jwt!
176+
-- :command :insert
177+
-- :result :affected
178+
-- :doc Insert a `:jwt` and a `:eviction-time` with `:one-time-id` into the blocklist.
179+
INSERT INTO blocked_jwt (
180+
jwt, evict_time, one_time_id
181+
) VALUES (
182+
:jwt, :eviction-time, :one-time-id
183+
);

src/db/postgres/lrsql/postgres/sql/query.sql

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ WHERE stmt.is_voided = FALSE
6767
--~ (when (:registration params) "AND stmt.registration = :registration")
6868
--~ (when (:authority-ifis params) "AND :frag:postgres-auth-subquery")
6969
--~ (if (:ascending? params) "ORDER BY stmt.id ASC" "ORDER BY stmt.id DESC")
70-
LIMIT :limit
70+
--~ (when (:limit params) "LIMIT :limit")
7171

7272
/* Note: We sort by both the PK and statement ID in order to force the query
7373
planner to avoid scanning on `stmt_a.id` first, which is much slower than
@@ -92,7 +92,7 @@ WHERE stmt_a.is_voided = FALSE
9292
--~ (when (:authority-ifis params) "AND :frag:postgres-auth-subquery")
9393
/*~ (if (:ascending? params) "ORDER BY (stmt_a.id, stmt_a.statement_id) ASC"
9494
"ORDER BY (stmt_a.id, stmt_a.statement_id) DESC") ~*/
95-
LIMIT :limit
95+
--~ (when (:limit params) "LIMIT :limit")
9696

9797
-- :name query-statements
9898
-- :command :query
@@ -107,7 +107,7 @@ FROM (
107107
(:frag:postgres-stmt-ref-subquery-frag))
108108
AS all_stmt
109109
--~ (if (:ascending? params) "ORDER BY all_stmt.id ASC" "ORDER BY all_stmt.id DESC")
110-
LIMIT :limit;
110+
--~ (when (:limit params) "LIMIT :limit")
111111

112112
/* Statement Object Queries */
113113

@@ -433,6 +433,15 @@ WHERE reaction_id IS NOT NULL;
433433
-- :name query-blocked-jwt-exists
434434
-- :command :query
435435
-- :result :one
436-
-- :doc Query that `:jwt` is in the blocklist.
436+
-- :doc Query that `:jwt` is in the blocklist. Excludes JWTs where `one_time_id` is not null.
437437
SELECT 1 FROM blocked_jwt
438-
WHERE jwt = :jwt;
438+
WHERE jwt = :jwt
439+
AND one_time_id IS NULL;
440+
441+
-- :name query-one-time-jwt-exists
442+
-- :command :query
443+
-- :result :one
444+
-- :doc Query that `:jwt` with `:one-time-id` exists.
445+
SELECT 1 FROM blocked_jwt
446+
WHERE jwt = :jwt
447+
AND one_time_id = :one-time-id;

0 commit comments

Comments
 (0)