Cookbook: UM - folio-org/folio-analytics GitHub Wiki
User Management
UM reporting includes all reports relating to user management. In this cookbook, some connections and specific SQL statements should be explained.
Table of Contents
- 1. Querying user data to get user informations
- 2. Fee fines
- 3. Querying user data to see user permissions
- 4. Querying user data for statistics
1. Querying user data to get user informations
If you want to retrieve user data in FOLIO, you should think carefully about what data you need beforehand. Some categories of a user record can appear multiple times in the FOLIO UI. This is the case, for example, when you enter addresses. You can enter multiple addresses for one user. Such data is stored in an array in FOLIO. If you querying these arrays, it can happen that you get multiple lines as a result for a user record.
Below we will show you some examples of how you can read these arrays. These queries are provided by Metadb as derived tables.
[top]
1.1. Users addresses
User addresses are stored in arrays because you can enter multiple addresses for one user.
Example:
SELECT
users.id AS user_id,
jsonb_extract_path_text(addresses.jsonb, 'id') AS address_id,
jsonb_extract_path_text(addresses.jsonb, 'countryId') AS address_country_id,
jsonb_extract_path_text(addresses.jsonb, 'addressLine1') AS address_line_1,
jsonb_extract_path_text(addresses.jsonb, 'addressLine2') AS address_line_2,
jsonb_extract_path_text(addresses.jsonb, 'city') AS address_city,
jsonb_extract_path_text(addresses.jsonb, 'region') AS address_region,
jsonb_extract_path_text(addresses.jsonb, 'postalCode') AS address_postal_code,
jsonb_extract_path_text(addresses.jsonb, 'addressTypeId')::uuid AS address_type_id,
addresstype__t.address_type AS address_type_name,
addresstype__t.desc AS address_type_description,
jsonb_extract_path_text(addresses.jsonb, 'primaryAddress')::boolean AS is_primary_address
FROM
folio_users.users
CROSS JOIN LATERAL jsonb_array_elements(jsonb_extract_path(jsonb, 'personal', 'addresses')) AS addresses (jsonb)
LEFT JOIN folio_users.addresstype__t ON addresstype__t.id = jsonb_extract_path_text(addresses.jsonb, 'addressTypeId')::uuid
[top]
1.2. users departments
Users can be assigned to departments. If you would like to see these users, please use the example below.
Example:
WITH departments_array AS (
SELECT
users.id AS user_id,
departments.jsonb #>> '{}' AS department_id,
departments.ordinality AS department_ordinality
FROM
folio_users.users
CROSS JOIN LATERAL jsonb_array_elements(jsonb_extract_path(jsonb, 'departments')) WITH ORDINALITY AS departments (jsonb)
)
SELECT
departments_array.user_id,
departments_array.department_id::uuid,
departments_array.department_ordinality,
departments__t.name AS department_name,
departments__t.code AS department_code
FROM
departments_array
LEFT JOIN folio_users.departments__t ON departments__t.id= departments_array.department_id::uuid
[top]
1.3. Users groups
Users can be assigned to groups. If you would like to see these users, please use the example below.
Example:
SELECT
users.id AS user_id,
users__t.patron_group :: UUID AS group_id,
groups__t.desc AS group_description,
groups__t.group AS group_name,
users__t.active,
users__t.barcode,
users__t.enrollment_date,
users__t.expiration_date,
jsonb_extract_path_text(users.jsonb, 'personal', 'lastName') AS user_last_name,
jsonb_extract_path_text(users.jsonb, 'personal', 'firstName') AS user_first_name,
jsonb_extract_path_text(users.jsonb, 'personal', 'middleName') AS user_middle_name
FROM
folio_users.users
LEFT JOIN folio_users.users__t ON users__t.id = users.id
JOIN folio_users.groups__t ON groups__t.id = users__t.patron_group
[top]
2. Fee fines
Users can get fees with different types of fee fines. The following example shows how you can query this information. You can also find the query as derived table feesfines_accounts_actions
.
Example:
SELECT
fine_account.id AS fine_account_id,
jsonb_extract_path_text(fine_account.jsonb, 'amount')::numeric(12,2) AS fine_account_amount,
jsonb_extract_path_text(fine_account.jsonb, 'dateCreated')::timestamptz AS fine_date,
jsonb_extract_path_text(fine_account.jsonb, 'dateUpdated')::timestamptz AS fine_updated_date,
jsonb_extract_path_text(fine_account.jsonb, 'feeFineId')::uuid AS fee_fine_id,
jsonb_extract_path_text(fine_account.jsonb, 'ownerId')::uuid AS owner_id,
jsonb_extract_path_text(fine_account.jsonb, 'feeFineOwner') AS fee_fine_owner,
jsonb_extract_path_text(fine_account.jsonb, 'feeFineType') AS fee_fine_type,
jsonb_extract_path_text(fine_account.jsonb, 'materialTypeId')::uuid AS material_type_id,
jsonb_extract_path_text(fine_account.jsonb, 'materialType') AS material_type,
jsonb_extract_path_text(fine_account.jsonb, 'payment_status') AS payment_status,
jsonb_extract_path_text(fine_account.jsonb, 'status', 'name') AS fine_status, -- open or closed
jsonb_extract_path_text(fine_account.jsonb, 'userId')::uuid AS account_user_id,
feefineactions.id AS transaction_id,
jsonb_extract_path_text(feefineactions.jsonb, 'accountId')::uuid AS account_id,
jsonb_extract_path_text(feefineactions.jsonb, 'amountAction')::numeric(12,2) AS transaction_amount,
jsonb_extract_path_text(feefineactions.jsonb, 'balance')::numeric(12,2) AS account_balance,
jsonb_extract_path_text(feefineactions.jsonb, 'typeAction') AS type_action,
jsonb_extract_path_text(feefineactions.jsonb, 'dateAction')::timestamptz AS transaction_date,
jsonb_extract_path_text(feefineactions.jsonb, 'createdAt') AS transaction_location,
jsonb_extract_path_text(feefineactions.jsonb, 'transactionInformation') AS transaction_information,
jsonb_extract_path_text(feefineactions.jsonb, 'source') AS operator_id,
jsonb_extract_path_text(feefineactions.jsonb, 'paymentMethod') AS payment_method,
users__t.id AS user_id,
users__t.patron_group AS user_patron_group_id,
user_groups.id AS patron_group_id,
user_groups.group AS patron_group_name
FROM
folio_feesfines.accounts AS fine_account
LEFT JOIN folio_feesfines.feefineactions ON fine_account.id = jsonb_extract_path_text(feefineactions.jsonb, 'accountId')::uuid
LEFT JOIN folio_users.users__t ON jsonb_extract_path_text(fine_account.jsonb, 'userId')::uuid = users__t.id
LEFT JOIN folio_users.groups__t AS user_groups ON users__t.patron_group = user_groups.id
ORDER BY
fine_account_id,
transaction_date
[top]
3. Querying user data to see user permissions
In FOLIO you can give certain permissions to a user. This is usually done via permission sets. In the following example you can see how you can retrieve these permission sets and who they have been assigned to.
Example:
WITH permissions_users AS (
SELECT
jsonb_array_elements_text(jsonb->'permissions') AS user_permissions,
jsonb_extract_path_text(jsonb, 'userId')::uuid AS user_id
FROM
folio_permissions.permissions_users
)
SELECT
permissions.id AS permission_id,
jsonb_extract_path_text(permissions.jsonb, 'displayName') AS permission_name,
jsonb_extract_path_text(permissions.jsonb, 'description') AS permission_description,
users.id AS user_id,
jsonb_extract_path_text(users.jsonb, 'personal', 'lastName') AS user_lastname,
jsonb_extract_path_text(users.jsonb, 'personal', 'firstName') AS user_firstname,
jsonb_extract_path_text(users.jsonb, 'barcode') AS user_barcode,
jsonb_extract_path_text(users.jsonb, 'username') AS user_username,
CASE WHEN jsonb_extract_path_text(users.jsonb, 'active')::boolean THEN 'active'
ELSE 'inactive'
END AS user_status,
jsonb_extract_path_text(user_groups.jsonb, 'group') AS user_patron_group,
jsonb_extract_path_text(users.jsonb, 'expirationDate') AS user_expiration_date
FROM
folio_permissions.permissions AS permissions
JOIN permissions_users ON permissions_users.user_permissions = permissions.id::varchar
LEFT JOIN folio_users.users AS users ON users.id = permissions_users.user_id
LEFT JOIN folio_users.groups AS user_groups ON user_groups.id = jsonb_extract_path_text(users.jsonb, 'patronGroup')::uuid
ORDER BY
permission_name,
permission_description,
user_lastname
[top]
4. Querying user data for statistics
If you want to make statistical queries, you can use simply the GROUP BY functionality. But it could also a good idea to use so-called window functions. With window functions you can split the dataset into different partitions. Then you can compare each record in each partition with an aggregate value or a calculated value of each partition. You can specify the PARTITION BY clause in the OVER function.
Below are 2 examples of how you can use this.
[top]
4.1. Number of users by user group
If you want to see the number of users by user group.
Example:
SELECT
users__t.patron_group :: UUID AS group_id,
groups__t.desc AS group_description,
groups__t.group AS group_name,
COUNT(users__t.id) AS count_by_group
FROM
folio_users.users__t
LEFT JOIN folio_users.groups__t ON groups__t.id = users__t.patron_group
GROUP BY
users__t.patron_group,
groups__t.desc,
groups__t.group
Sample Output:
group_id | group_description | group_name | count_by_group |
---|---|---|---|
3684a786-6671-4268-8ed0-9db82ebca60b | Staff Member | staff | 11 |
503a81cd-6c26-400f-b620-14c08943697c | Faculty Member | faculty | 1 |
bdc2b6d4-5ceb-4a12-ab46-249b9a68473e | Undergradate Student | undergrad | 1 |
NULL | NULL | NULL | 5 |
[top]
4.2. Average age by user group
If you want to see the average age by user group.
Example:
SELECT
users.id AS user_id,
users__t.patron_group :: UUID AS group_id,
groups__t.desc AS group_description,
groups__t.group AS group_name,
users__t.active,
users__t.barcode,
users__t.enrollment_date,
users__t.expiration_date,
jsonb_extract_path_text(users.jsonb, 'personal', 'lastName') AS user_last_name,
jsonb_extract_path_text(users.jsonb, 'personal', 'firstName') AS user_first_name,
jsonb_extract_path_text(users.jsonb, 'personal', 'middleName') AS user_middle_name,
COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date AS user_date_of_birth,
AGE(COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date) AS age,
AVG(AGE(COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date)) OVER (PARTITION BY groups__t.group) AS average_age_by_group,
MAX(AGE(COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date)) OVER (PARTITION BY groups__t.group) AS max_age_by_group,
MIN(AGE(COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date)) OVER (PARTITION BY groups__t.group) AS min_age_by_group
FROM
folio_users.users
LEFT JOIN folio_users.users__t ON users__t.id = users.id
LEFT JOIN folio_users.groups__t ON groups__t.id = users__t.patron_group
GROUP BY
users.id,
users__t.patron_group,
groups__t.desc,
groups__t.group,
users__t.active,
users__t.barcode,
users__t.enrollment_date,
users__t.expiration_date,
jsonb_extract_path_text(users.jsonb, 'personal', 'lastName'),
jsonb_extract_path_text(users.jsonb, 'personal', 'firstName'),
jsonb_extract_path_text(users.jsonb, 'personal', 'middleName'),
jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'),
jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth')
Sample Output (a part of it):
user_id | group_id | group_name | user_date_of_birth | age | average_age_by_group | max_age_by_group | min_age_by_group |
---|---|---|---|---|---|---|---|
caf3bb4a-b6b2-4e91-b14c-fbec128ac509 | 503a81cd-6c26-400f-b620-14c08943697c | faculty | 1900-01-01 | 124 years 2 mons | 124 years 2 mons | 124 years 2 mons | 124 years 2 mons |
67adb68e-232f-4cdf-8bce-faf09093071a | 3684a786-6671-4268-8ed0-9db82ebca60b | staff | 1987-01-01 | 37 years 2 mons | 116 years 3 mons 2 days 17:27:16.3872 | 124 years 2 mons | 37 years 2 mons |
e45fccb3-1f39-4125-a39d-a573cda300d9 | 3684a786-6671-4268-8ed0-9db82ebca60b | staff | 1900-01-01 | 124 years 2 mons | 116 years 3 mons 2 days 17:27:16.3872 | 124 years 2 mons | 37 years 2 mons |
As you can see, age is calculated depending on group membership. Please note: In this example, NULL values have been replaced. Usually the date of birth should be present.
Example (without user data):
WITH user_date_of_birth AS (
SELECT
users.id AS user_id,
AGE(COALESCE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth'), '1900-01-01') :: date) AS date_of_birth
FROM
folio_users.users
)
SELECT
users__t.patron_group :: UUID AS group_id,
groups__t.desc AS group_description,
groups__t.group AS group_name,
AVG(user_date_of_birth.date_of_birth) AS average_age_by_group,
MAX(user_date_of_birth.date_of_birth) AS max_age_by_group,
MIN(user_date_of_birth.date_of_birth) AS min_age_by_group,
COUNT(users__t.id) AS count_users
FROM
folio_users.users__t
LEFT JOIN user_date_of_birth ON user_date_of_birth.user_id = users__t.id
LEFT JOIN folio_users.groups__t ON groups__t.id = users__t.patron_group
GROUP BY
users__t.patron_group,
groups__t.desc,
groups__t.group
Sample Output:
group_id | group_description | group_name | average_age_by_group | max_age_by_group | min_age_by_group | count_users |
---|---|---|---|---|---|---|
3684a786-6671-4268-8ed0-9db82ebca60b | Staff Member | staff | 116 years 3 mons 2 days 17:27:16.3872 | 124 years 2 mons | 37 years 2 mons | 11 |
503a81cd-6c26-400f-b620-14c08943697c | Faculty Member | faculty | 124 years 2 mons | 124 years 2 mons | 124 years 2 mons | 1 |
bdc2b6d4-5ceb-4a12-ab46-249b9a68473e | Undergradate Student | undergrad | 124 years 2 mons | 124 years 2 mons | 124 years 2 mons | 1 |
NULL | NULL | NULL | 124 years 2 mons | 124 years 2 mons | 124 years 2 mons | 5 |
[top]
4.3. Number of users by age and user category
If you want the number of users by age and user category, you must first calculate the age of each person in a CTE. After that you create another CTE for each age category and use the age previously calculated from the CTE as the basis for the WHERE clause. Then you need to add this CTE using Left Join to your main query where you have grouped the user groups.
Example:
WITH user_date_of_birth AS (
SELECT
users.id AS user_id,
AGE(jsonb_extract_path_text(users.jsonb, 'personal', 'dateOfBirth') :: date) AS date_of_birth
FROM
folio_users.users
),
users_between_30_and_39 AS (
SELECT
COUNT(users.id) AS counts,
groups.id AS group_id
FROM
folio_users.users
LEFT JOIN user_date_of_birth ON user_date_of_birth.user_id = users.id
LEFT JOIN folio_users.groups ON groups.id = jsonb_extract_path_text(users.jsonb, 'patronGroup') :: UUID
WHERE
EXTRACT('YEAR' FROM date_of_birth) >=30
AND
EXTRACT('YEAR' FROM date_of_birth) <=39
GROUP BY
groups.id
)
SELECT
jsonb_extract_path_text(users.jsonb, 'patronGroup') :: UUID AS group_id,
jsonb_extract_path_text(groups.jsonb, 'desc') AS group_description,
jsonb_extract_path_text(groups.jsonb, 'group') AS group_name,
AVG(user_date_of_birth.date_of_birth) AS average_age_by_group,
MAX(user_date_of_birth.date_of_birth) AS max_age_by_group,
MIN(user_date_of_birth.date_of_birth) AS min_age_by_group,
COUNT(users.id) AS count_users,
COALESCE(users_between_30_and_39.counts, 0) AS cat30to39
FROM
folio_users.users
LEFT JOIN user_date_of_birth ON user_date_of_birth.user_id = users.id
LEFT JOIN folio_users.groups ON groups.id = jsonb_extract_path_text(users.jsonb, 'patronGroup') :: UUID
LEFT JOIN users_between_30_and_39 ON users_between_30_and_39.group_id = groups.id
GROUP BY
jsonb_extract_path_text(users.jsonb, 'patronGroup'),
jsonb_extract_path_text(groups.jsonb, 'desc'),
jsonb_extract_path_text(groups.jsonb, 'group'),
users_between_30_and_39.counts
Sample Output:
group_id | group_description | group_name | average_age_by_group | max_age_by_group | min_age_by_group | count_users | cat30to39 |
---|---|---|---|---|---|---|---|
3684a786-6671-4268-8ed0-9db82ebca60b | Staff Member | staff | 45 years 2 mons | 57 years 4 mons | 37 years 2 mons | 11 | 1 |
[top]