diff --git a/backend/graphql/notification_config/src/mutations/update.rs b/backend/graphql/notification_config/src/mutations/update.rs index c3050986..80e11f00 100644 --- a/backend/graphql/notification_config/src/mutations/update.rs +++ b/backend/graphql/notification_config/src/mutations/update.rs @@ -15,6 +15,7 @@ pub struct UpdateNotificationConfigInput { pub title: Option, pub configuration_data: Option, pub parameters: Option, + pub parameter_query_id: Option, pub status: Option, pub recipient_ids: Option>, pub recipient_list_ids: Option>, @@ -54,6 +55,7 @@ impl From for UpdateNotificationConfig { configuration_data, status, parameters, + parameter_query_id, recipient_ids, recipient_list_ids, sql_recipient_list_ids, @@ -66,6 +68,7 @@ impl From for UpdateNotificationConfig { configuration_data, status: status.map(ConfigStatus::to_domain), parameters, + parameter_query_id, recipient_ids, recipient_list_ids, sql_recipient_list_ids, diff --git a/backend/graphql/types/src/types/notification_config.rs b/backend/graphql/types/src/types/notification_config.rs index 762817f4..719f252e 100644 --- a/backend/graphql/types/src/types/notification_config.rs +++ b/backend/graphql/types/src/types/notification_config.rs @@ -44,6 +44,10 @@ impl NotificationConfigNode { &self.row().parameters } + pub async fn parameter_query_id(&self) -> &Option { + &self.row().parameter_query_id + } + pub async fn recipient_ids(&self) -> &[String] { &self.row().recipient_ids } diff --git a/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/down.sql b/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/down.sql new file mode 100644 index 00000000..d9a93fe9 --- /dev/null +++ b/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/up.sql b/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/up.sql new file mode 100644 index 00000000..060f0157 --- /dev/null +++ b/backend/repository/migrations/2024-02-07-154900_add_parameter_query_id/up.sql @@ -0,0 +1 @@ +ALTER TABLE notification_config ADD COLUMN parameter_query_id TEXT; diff --git a/backend/repository/src/db_diesel/notification_config_row.rs b/backend/repository/src/db_diesel/notification_config_row.rs index 50ef1ac9..5716ad98 100644 --- a/backend/repository/src/db_diesel/notification_config_row.rs +++ b/backend/repository/src/db_diesel/notification_config_row.rs @@ -14,6 +14,7 @@ table! { configuration_data -> Text, status -> crate::db_diesel::notification_config_row::NotificationConfigStatusMapping, parameters -> Text, + parameter_query_id -> Nullable, recipient_ids -> Text, recipient_list_ids -> Text, sql_recipient_list_ids -> Text, @@ -86,6 +87,7 @@ pub struct NotificationConfigRow { pub configuration_data: String, pub status: NotificationConfigStatus, pub parameters: String, // JSON object {key: "value"} + pub parameter_query_id: Option, pub recipient_ids: String, // JSON array of strings (ids) pub recipient_list_ids: String, // JSON array of strings (ids) pub sql_recipient_list_ids: String, // JSON array of strings (ids) diff --git a/backend/scheduled/src/process.rs b/backend/scheduled/src/process.rs index 4aa8e3db..4945f445 100644 --- a/backend/scheduled/src/process.rs +++ b/backend/scheduled/src/process.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use repository::{NotificationConfigKind, NotificationConfigRowRepository}; use service::{ notification::enqueue::{create_notification_events, NotificationContext, TemplateDefinition}, - notification_config::{query::NotificationConfig, recipients::get_notification_targets}, + notification_config::{query::NotificationConfig, recipients::get_notification_targets, parameters::get_notification_parameters}, service_provider::ServiceContext, }; @@ -118,19 +118,12 @@ fn try_process_scheduled_notifications( ))); } - let params = match scheduled_notification.parameters.len() { - 0 => "[{}]".to_string(), - _ => scheduled_notification.parameters.clone(), + let param_results = get_notification_parameters(ctx, &scheduled_notification); + let mut all_params = match param_results { + Ok(val) => val, + Err(e) => return Err(NotificationError::InternalError(format!("Failed to fetch parameters: {:?}", e))) }; - let mut all_params: Vec> = serde_json::from_str(¶ms) - .map_err(|e| { - NotificationError::InternalError(format!( - "Failed to parse notification config parameters (expecting an array of params): {:?} - {}", - e, params - )) - })?; - if all_params.len() == 0 { // If no parameters are provided, create a single empty parameter set all_params = vec![HashMap::new()]; diff --git a/backend/service/src/notification_config/create.rs b/backend/service/src/notification_config/create.rs index cb027722..ed2d5b88 100644 --- a/backend/service/src/notification_config/create.rs +++ b/backend/service/src/notification_config/create.rs @@ -66,6 +66,7 @@ pub fn generate( configuration_data: "{}".to_string(), status: NotificationConfigStatus::Disabled, parameters: "{}".to_string(), + parameter_query_id: None, recipient_ids: "[]".to_string(), recipient_list_ids: "[]".to_string(), sql_recipient_list_ids: "[]".to_string(), diff --git a/backend/service/src/notification_config/mod.rs b/backend/service/src/notification_config/mod.rs index 4ed21c9b..a18f891e 100644 --- a/backend/service/src/notification_config/mod.rs +++ b/backend/service/src/notification_config/mod.rs @@ -22,6 +22,7 @@ pub mod create; pub mod delete; pub mod duplicate; pub mod intervals; +pub mod parameters; pub mod query; pub mod recipients; pub mod update; diff --git a/backend/service/src/notification_config/parameters.rs b/backend/service/src/notification_config/parameters.rs new file mode 100644 index 00000000..3a186ffe --- /dev/null +++ b/backend/service/src/notification_config/parameters.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; + +use crate::service_provider::ServiceContext; +use crate::notification::NotificationServiceError; +use super::query::NotificationConfig; +use repository::NotificationQueryRowRepository; + +pub fn get_notification_parameters( + ctx: &ServiceContext, + notification_config: &NotificationConfig, +) -> Result>, NotificationServiceError> { + // Fetch default parameters from config + let params_string = match notification_config.parameters.len() { + 0 => "[]".to_string(), + _ => notification_config.parameters.clone(), + }; + let sql_params_string = match ¬ification_config.parameter_query_id { + None => "[]".to_string(), + Some(query_id) => get_sql_parameters(ctx, query_id)?, + }; + + // Parse both sets of parameter strings, then merge the resulting vectors + let mut all_params: Vec> = serde_json::from_str(¶ms_string) + .map_err(|e| { + NotificationServiceError::InternalError(format!( + "Failed to parse notification config parameters (expecting an array of params_string): {:?} - {}", + e, params_string + )) + })?; + let sql_params: Vec> = serde_json::from_str(&sql_params_string) + .map_err(|e| { + NotificationServiceError::InternalError(format!( + "Failed to parse notification sql parameters (expecting an array of params_string): {:?} - {}", + e, params_string + )) + })?; + all_params.extend(sql_params); + + return Ok(all_params); +} + +fn get_sql_parameters( + ctx: &ServiceContext, + parameter_query_id: &String, +) -> Result { + // TODO: Maybe split these to a new database table + let repository = NotificationQueryRowRepository::new(&ctx.connection); + let query_record = repository.find_one_by_id(¶meter_query_id)?; + + let sql_query = match query_record { + None => return Err(NotificationServiceError::InternalError(format!( + "No query found for parameter_query_id: {}", parameter_query_id) + )), + Some(record) => record.query, + }; + + let query_result = ctx + .service_provider + .datasource_service + .run_sql_query(sql_query) + .map_err(|e| { + NotificationServiceError::InternalError(format!("Error when fetching parameter_query_id: {} - {:?}", + parameter_query_id, e + )) + })?; + + return Ok(query_result.results); +} diff --git a/backend/service/src/notification_config/query.rs b/backend/service/src/notification_config/query.rs index 931a002b..1ff6324e 100644 --- a/backend/service/src/notification_config/query.rs +++ b/backend/service/src/notification_config/query.rs @@ -22,6 +22,7 @@ pub struct NotificationConfig { pub configuration_data: String, pub status: NotificationConfigStatus, pub parameters: String, + pub parameter_query_id: Option, pub recipient_ids: Vec, pub recipient_list_ids: Vec, pub sql_recipient_list_ids: Vec, @@ -38,6 +39,7 @@ impl From for NotificationConfig { configuration_data, status, parameters, + parameter_query_id, recipient_ids, recipient_list_ids, sql_recipient_list_ids, @@ -52,6 +54,7 @@ impl From for NotificationConfig { configuration_data, status, parameters, + parameter_query_id, recipient_ids: serde_json::from_str(&recipient_ids).unwrap_or_default(), recipient_list_ids: serde_json::from_str(&recipient_list_ids).unwrap_or_default(), sql_recipient_list_ids: serde_json::from_str(&sql_recipient_list_ids) diff --git a/backend/service/src/notification_config/update.rs b/backend/service/src/notification_config/update.rs index 91b64559..bd747f71 100644 --- a/backend/service/src/notification_config/update.rs +++ b/backend/service/src/notification_config/update.rs @@ -17,6 +17,7 @@ pub struct UpdateNotificationConfig { pub configuration_data: Option, pub status: Option, pub parameters: Option, + pub parameter_query_id: Option, pub recipient_ids: Option>, pub recipient_list_ids: Option>, pub sql_recipient_list_ids: Option>, @@ -71,6 +72,7 @@ pub fn generate( configuration_data, status, parameters, + parameter_query_id, recipient_ids, recipient_list_ids, sql_recipient_list_ids, @@ -124,5 +126,8 @@ pub fn generate( // Note: We usually reset the next check datetime in case the schedule has changed, or something needs to be recalculated new_notification_config_row.next_due_datetime = next_due_datetime; + // We might want to clear out the parameter_query_id so it's allowed to be None + new_notification_config_row.parameter_query_id = parameter_query_id; + Ok(new_notification_config_row) } diff --git a/frontend/packages/common/src/intl/locales/en/system.json b/frontend/packages/common/src/intl/locales/en/system.json index c36f9cb2..dffadfc3 100644 --- a/frontend/packages/common/src/intl/locales/en/system.json +++ b/frontend/packages/common/src/intl/locales/en/system.json @@ -38,6 +38,7 @@ "label.edit-user": "Edit User", "label.edit-parameters": "Edit Parameters", "label.parameters-as-json": "JSON Parameters", + "label.parameter-query-select": "Get parameters from SQL Query", "label.error": "Error", "label.filter-by-notification-config": "Filter by Notification Config", "label.generated-notification": "Generated Notification", @@ -108,4 +109,4 @@ "messages.confirm-duplicate": "Do you want to make a copy of this configuration?", "button.edit-notification-config": "Edit Notification Config", "messages.no-events-matching-status": "No events found" -} \ No newline at end of file +} diff --git a/frontend/packages/common/src/types/schema.ts b/frontend/packages/common/src/types/schema.ts index cdd39f7a..468dd23d 100644 --- a/frontend/packages/common/src/types/schema.ts +++ b/frontend/packages/common/src/types/schema.ts @@ -633,6 +633,7 @@ export type NotificationConfigNode = { configurationData: Scalars['String']['output']; id: Scalars['String']['output']; kind: ConfigKind; + parameterQueryId?: Maybe; parameters: Scalars['String']['output']; recipientIds: Array; recipientListIds: Array; @@ -950,6 +951,7 @@ export type UpdateNotificationConfigInput = { configurationData?: InputMaybe; id: Scalars['String']['input']; nextDueDatetime?: InputMaybe; + parameterQueryId?: InputMaybe; parameters?: InputMaybe; recipientIds?: InputMaybe>; recipientListIds?: InputMaybe>; diff --git a/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx index a9a3e2ee..e8df12d5 100644 --- a/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx @@ -172,6 +172,8 @@ export const BaseNotificationEditPage = ({ allowParameterSets={allowParameterSets} onUpdateParams={onUpdateParams} onDeleteParam={onDeleteParam} + onChangeParameterQuery={x => onUpdate({ parameterQueryId: x } as Partial)} + parameterQueryId={draft.parameterQueryId} /> void; onDeleteParam: (idx: number, key: string | null) => void; // Warning: null deletes everything for that index + onChangeParameterQuery?: (id: string | null) => void; + parameterQueryId?: string | null; } export const NotificationDetailPanel = ({ @@ -26,6 +31,8 @@ export const NotificationDetailPanel = ({ allowParameterSets = false, onUpdateParams, onDeleteParam, + onChangeParameterQuery = () => {}, + parameterQueryId = null, }: ParamsPanelProps) => { const t = useTranslation('system'); @@ -37,48 +44,68 @@ export const NotificationDetailPanel = ({ params = [params]; } - if (params.length === 0 || params[0] === undefined) { - params = [{} as KeyedParams]; - } + const { queryParams } = useQueryParamsState(); + const { data: queriesData } = useNotificationQueries(queryParams); + const selectedQuery = queriesData?.nodes.find(query => query.id === parameterQueryId); const paramEditors = ( <> - {params.map((_, idx) => { - return ( + { + params.length === 0 ? ( { - params.push(params[idx] ?? {}); - onDeleteParam(idx + 1, 'this-is-a-hack-to-force-an-update'); + params.push({}); + onDeleteParam(0, 'this-is-a-hack-to-force-an-update'); }} - disabled={!allowParameterSets} - icon={} - label={t('button.duplicate')} - /> - onDeleteParam(idx, null)} - disabled={params.length === 1} - icon={} - label={t('label.delete')} + icon={} + label={t('button.create')} /> } - > - onUpdateParams(idx, key, value)} - onDeleteParam={key => onDeleteParam(idx, key)} - /> - - ); - })} + /> + ) : + params.map((_, idx) => { + return ( + + { + params.push(params[idx] ?? {}); + onDeleteParam(idx + 1, 'this-is-a-hack-to-force-an-update'); + }} + disabled={!allowParameterSets} + icon={} + label={t('button.duplicate')} + /> + onDeleteParam(idx, null)} + icon={} + label={t('label.delete')} + /> + + } + > + onUpdateParams(idx, key, value)} + onDeleteParam={key => onDeleteParam(idx, key)} + /> + + ); + }) + } ); @@ -112,10 +139,25 @@ export const NotificationDetailPanel = ({ try { const editedParams: KeyedParams[] = JSON.parse(paramsString); editedParams.forEach((param, idx) => { - for (const key of Object.keys(param)) { - onUpdateParams(idx, key, param[key] ?? ''); + if (idx === params.length) { + // Allow adding empty sets from JSON + params.push({}); + onDeleteParam(idx, 'this-is-a-hack-to-force-an-update'); + } + // Iterate through all valid keys + for (const key of [...Object.keys(param), ...Object.keys(params[idx] || {})]) { + if (param[key]) { + // If we parsed the key from the JSON input, update it + onUpdateParams(idx, key, param[key] || ''); + } else { + // If the key exists on the parameter object but not the JSON, delete it + onDeleteParam(idx, key); + } } }); + while (params.length > editedParams.length) { + onDeleteParam(editedParams.length, null); + } } catch (e) { setErrorMessage(`Unable to save new parameters: ${e}`); } @@ -141,10 +183,26 @@ export const NotificationDetailPanel = ({ ); + const parameterQuerySelector = ( + + option.name} + onChange={(_, option) => onChangeParameterQuery(option?.id ?? null)} + value={selectedQuery ? { ...selectedQuery, label: selectedQuery.name } : null} + /> + + ); + return ( {paramEditors} {jsonParamsEditor} + {parameterQuerySelector} ); }; diff --git a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx index 560674df..509ef08a 100644 --- a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx @@ -44,6 +44,10 @@ export const ScheduledNotificationEditPage = () => { (entity as NotificationConfigRowFragment) ?? null, parsingErrorSnack ); + if (parsedDraft) { + // Backend expects this value to be null if unedited so don't load it from config + parsedDraft.nextDueDatetime = null; + } setDraft(parsedDraft ?? defaultSchedulerNotification); if (parsedDraft?.title) setSuffix(parsedDraft?.title); }, [data]); diff --git a/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts b/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts index 1b4ba7be..20bec57a 100644 --- a/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts +++ b/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts @@ -45,6 +45,7 @@ export const defaultSchedulerNotification: ScheduledNotification = { recipientIds: [], sqlRecipientListIds: [], parameters: '[]', + parameterQueryId: null, parsedParameters: [], requiredParameters: [], scheduleFrequency: 'daily', @@ -75,6 +76,7 @@ export function buildScheduledNotificationInputs( configurationData: JSON.stringify(config), status: config.status, parameters: JSON.stringify(params), + parameterQueryId: config.parameterQueryId, recipientIds: config.recipientIds, recipientListIds: config.recipientListIds, sqlRecipientListIds: config.sqlRecipientListIds, diff --git a/frontend/packages/system/src/Notifications/api/operations.generated.ts b/frontend/packages/system/src/Notifications/api/operations.generated.ts index 257a678b..b91608fd 100644 --- a/frontend/packages/system/src/Notifications/api/operations.generated.ts +++ b/frontend/packages/system/src/Notifications/api/operations.generated.ts @@ -3,7 +3,7 @@ import * as Types from '@notify-frontend/common'; import { GraphQLClient } from 'graphql-request'; import * as Dom from 'graphql-request/dist/types.dom'; import gql from 'graphql-tag'; -export type NotificationConfigRowFragment = { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array }; +export type NotificationConfigRowFragment = { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, parameterQueryId?: string | null, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array }; export type NotificationConfigsQueryVariables = Types.Exact<{ filter?: Types.InputMaybe; @@ -12,21 +12,21 @@ export type NotificationConfigsQueryVariables = Types.Exact<{ }>; -export type NotificationConfigsQuery = { __typename: 'FullQuery', notificationConfigs: { __typename: 'NotificationConfigConnector', totalCount: number, nodes: Array<{ __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array }> } }; +export type NotificationConfigsQuery = { __typename: 'FullQuery', notificationConfigs: { __typename: 'NotificationConfigConnector', totalCount: number, nodes: Array<{ __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, parameterQueryId?: string | null, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array }> } }; export type CreateNotificationConfigMutationVariables = Types.Exact<{ input: Types.CreateNotificationConfigInput; }>; -export type CreateNotificationConfigMutation = { __typename: 'FullMutation', createNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; +export type CreateNotificationConfigMutation = { __typename: 'FullMutation', createNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, parameterQueryId?: string | null, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; export type UpdateNotificationConfigMutationVariables = Types.Exact<{ input: Types.UpdateNotificationConfigInput; }>; -export type UpdateNotificationConfigMutation = { __typename: 'FullMutation', updateNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; +export type UpdateNotificationConfigMutation = { __typename: 'FullMutation', updateNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, parameterQueryId?: string | null, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; export type DeleteNotificationConfigMutationVariables = Types.Exact<{ id: Types.Scalars['String']['input']; @@ -40,7 +40,7 @@ export type DuplicateNotificationConfigMutationVariables = Types.Exact<{ }>; -export type DuplicateNotificationConfigMutation = { __typename: 'FullMutation', duplicateNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; +export type DuplicateNotificationConfigMutation = { __typename: 'FullMutation', duplicateNotificationConfig: { __typename: 'NotificationConfigNode', id: string, title: string, kind: Types.ConfigKind, configurationData: string, status: Types.ConfigStatus, parameters: string, parameterQueryId?: string | null, recipientIds: Array, recipientListIds: Array, sqlRecipientListIds: Array } }; export type RunSqlQueryWithParametersQueryVariables = Types.Exact<{ sqlQuery?: Types.InputMaybe; @@ -58,6 +58,7 @@ export const NotificationConfigRowFragmentDoc = gql` configurationData status parameters + parameterQueryId recipientIds recipientListIds sqlRecipientListIds diff --git a/frontend/packages/system/src/Notifications/api/operations.graphql b/frontend/packages/system/src/Notifications/api/operations.graphql index 77d2392a..93a9abb9 100644 --- a/frontend/packages/system/src/Notifications/api/operations.graphql +++ b/frontend/packages/system/src/Notifications/api/operations.graphql @@ -5,6 +5,7 @@ fragment NotificationConfigRow on NotificationConfigNode { configurationData status parameters + parameterQueryId recipientIds recipientListIds sqlRecipientListIds diff --git a/frontend/packages/system/src/Notifications/types.ts b/frontend/packages/system/src/Notifications/types.ts index ca6fbedc..ea76862b 100644 --- a/frontend/packages/system/src/Notifications/types.ts +++ b/frontend/packages/system/src/Notifications/types.ts @@ -41,6 +41,7 @@ type BaseConfig = Pick< | 'title' | 'status' | 'parameters' + | 'parameterQueryId' | 'recipientIds' | 'recipientListIds' | 'sqlRecipientListIds'