From 14007f30cc6eb1119000ada8eac51939558b171f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 11 Jul 2024 15:10:31 -0500 Subject: [PATCH 01/21] Refactor to use nfydest package for destination types - Replaced type definitions with nfydest equivalents - Updated type info and validation methods - Aligned dynamic parameters and fields with new interfaces - Adjusted imports and model mappings in graphql schema --- graphql2/generated.go | 111 +++++++++-------- graphql2/gqlgen.yml | 6 + graphql2/graphqlapp/destinationtypes.go | 159 +++++++++++++----------- graphql2/models_gen.go | 62 --------- notification/nfydest/provider.go | 42 +++++++ notification/nfydest/typeinfo.go | 61 +++++++++ 6 files changed, 248 insertions(+), 193 deletions(-) create mode 100644 notification/nfydest/provider.go create mode 100644 notification/nfydest/typeinfo.go diff --git a/graphql2/generated.go b/graphql2/generated.go index eda4dd3267..9a370ad510 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -30,6 +30,7 @@ import ( "github.com/target/goalert/limit" "github.com/target/goalert/notice" "github.com/target/goalert/notification" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/notification/slack" "github.com/target/goalert/notification/twilio" "github.com/target/goalert/oncall" @@ -1035,7 +1036,7 @@ type QueryResolver interface { GenerateSlackAppManifest(ctx context.Context) (string, error) LinkAccountInfo(ctx context.Context, token string) (*LinkAccountInfo, error) SwoStatus(ctx context.Context) (*SWOStatus, error) - DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]DestinationTypeInfo, error) + DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]nfydest.TypeInfo, error) DestinationFieldValidate(ctx context.Context, input DestinationFieldValidateInput) (bool, error) DestinationFieldSearch(ctx context.Context, input DestinationFieldSearchInput) (*FieldSearchConnection, error) DestinationFieldValueName(ctx context.Context, input DestinationFieldValidateInput) (string, error) @@ -11097,7 +11098,7 @@ func (ec *executionContext) fieldContext_DestinationDisplayInfoError_error(_ con return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_fieldID(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_fieldID(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_fieldID(ctx, field) if err != nil { return graphql.Null @@ -11141,7 +11142,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_fieldID(_ contex return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_label(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_label(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_label(ctx, field) if err != nil { return graphql.Null @@ -11185,7 +11186,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_label(_ context. return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_hint(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_hint(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_hint(ctx, field) if err != nil { return graphql.Null @@ -11229,7 +11230,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_hint(_ context.C return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_hintURL(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_hintURL(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_hintURL(ctx, field) if err != nil { return graphql.Null @@ -11273,7 +11274,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_hintURL(_ contex return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_placeholderText(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_placeholderText(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_placeholderText(ctx, field) if err != nil { return graphql.Null @@ -11317,7 +11318,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_placeholderText( return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_prefix(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_prefix(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_prefix(ctx, field) if err != nil { return graphql.Null @@ -11361,7 +11362,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_prefix(_ context return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_inputType(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_inputType(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_inputType(ctx, field) if err != nil { return graphql.Null @@ -11405,7 +11406,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_inputType(_ cont return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_supportsSearch(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_supportsSearch(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_supportsSearch(ctx, field) if err != nil { return graphql.Null @@ -11449,7 +11450,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_supportsSearch(_ return fc, nil } -func (ec *executionContext) _DestinationFieldConfig_supportsValidation(ctx context.Context, field graphql.CollectedField, obj *DestinationFieldConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationFieldConfig_supportsValidation(ctx context.Context, field graphql.CollectedField, obj *nfydest.FieldConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationFieldConfig_supportsValidation(ctx, field) if err != nil { return graphql.Null @@ -11493,7 +11494,7 @@ func (ec *executionContext) fieldContext_DestinationFieldConfig_supportsValidati return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_type(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_type(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_type(ctx, field) if err != nil { return graphql.Null @@ -11537,7 +11538,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_type(_ context.Cont return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_name(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_name(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_name(ctx, field) if err != nil { return graphql.Null @@ -11581,7 +11582,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_name(_ context.Cont return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_iconURL(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_iconURL(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_iconURL(ctx, field) if err != nil { return graphql.Null @@ -11625,7 +11626,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_iconURL(_ context.C return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_iconAltText(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_iconAltText(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_iconAltText(ctx, field) if err != nil { return graphql.Null @@ -11669,7 +11670,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_iconAltText(_ conte return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_enabled(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_enabled(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_enabled(ctx, field) if err != nil { return graphql.Null @@ -11713,7 +11714,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_enabled(_ context.C return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_requiredFields(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_requiredFields(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_requiredFields(ctx, field) if err != nil { return graphql.Null @@ -11739,9 +11740,9 @@ func (ec *executionContext) _DestinationTypeInfo_requiredFields(ctx context.Cont } return graphql.Null } - res := resTmp.([]DestinationFieldConfig) + res := resTmp.([]nfydest.FieldConfig) fc.Result = res - return ec.marshalNDestinationFieldConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationFieldConfigᚄ(ctx, field.Selections, res) + return ec.marshalNDestinationFieldConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐFieldConfigᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_DestinationTypeInfo_requiredFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -11777,7 +11778,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_requiredFields(_ co return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_dynamicParams(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_dynamicParams(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_dynamicParams(ctx, field) if err != nil { return graphql.Null @@ -11803,9 +11804,9 @@ func (ec *executionContext) _DestinationTypeInfo_dynamicParams(ctx context.Conte } return graphql.Null } - res := resTmp.([]DynamicParamConfig) + res := resTmp.([]nfydest.DynamicParamConfig) fc.Result = res - return ec.marshalNDynamicParamConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDynamicParamConfigᚄ(ctx, field.Selections, res) + return ec.marshalNDynamicParamConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDynamicParamConfigᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_DestinationTypeInfo_dynamicParams(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -11833,7 +11834,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_dynamicParams(_ con return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_userDisclaimer(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_userDisclaimer(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_userDisclaimer(ctx, field) if err != nil { return graphql.Null @@ -11877,7 +11878,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_userDisclaimer(_ co return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_isContactMethod(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_isContactMethod(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_isContactMethod(ctx, field) if err != nil { return graphql.Null @@ -11891,7 +11892,7 @@ func (ec *executionContext) _DestinationTypeInfo_isContactMethod(ctx context.Con }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.IsContactMethod, nil + return obj.IsContactMethod(), nil }) if err != nil { ec.Error(ctx, err) @@ -11912,7 +11913,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isContactMethod(_ c fc = &graphql.FieldContext{ Object: "DestinationTypeInfo", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") @@ -11921,7 +11922,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isContactMethod(_ c return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_isEPTarget(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_isEPTarget(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_isEPTarget(ctx, field) if err != nil { return graphql.Null @@ -11935,7 +11936,7 @@ func (ec *executionContext) _DestinationTypeInfo_isEPTarget(ctx context.Context, }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.IsEPTarget, nil + return obj.IsEPTarget(), nil }) if err != nil { ec.Error(ctx, err) @@ -11956,7 +11957,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isEPTarget(_ contex fc = &graphql.FieldContext{ Object: "DestinationTypeInfo", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") @@ -11965,7 +11966,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isEPTarget(_ contex return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_isSchedOnCallNotify(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_isSchedOnCallNotify(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_isSchedOnCallNotify(ctx, field) if err != nil { return graphql.Null @@ -11979,7 +11980,7 @@ func (ec *executionContext) _DestinationTypeInfo_isSchedOnCallNotify(ctx context }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.IsSchedOnCallNotify, nil + return obj.IsSchedOnCallNotify(), nil }) if err != nil { ec.Error(ctx, err) @@ -12000,7 +12001,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isSchedOnCallNotify fc = &graphql.FieldContext{ Object: "DestinationTypeInfo", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") @@ -12009,7 +12010,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isSchedOnCallNotify return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_isDynamicAction(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_isDynamicAction(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_isDynamicAction(ctx, field) if err != nil { return graphql.Null @@ -12023,7 +12024,7 @@ func (ec *executionContext) _DestinationTypeInfo_isDynamicAction(ctx context.Con }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.IsDynamicAction, nil + return obj.IsDynamicAction(), nil }) if err != nil { ec.Error(ctx, err) @@ -12044,7 +12045,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isDynamicAction(_ c fc = &graphql.FieldContext{ Object: "DestinationTypeInfo", Field: field, - IsMethod: false, + IsMethod: true, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Boolean does not have child fields") @@ -12053,7 +12054,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_isDynamicAction(_ c return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_supportsStatusUpdates(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_supportsStatusUpdates(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_supportsStatusUpdates(ctx, field) if err != nil { return graphql.Null @@ -12097,7 +12098,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_supportsStatusUpdat return fc, nil } -func (ec *executionContext) _DestinationTypeInfo_statusUpdatesRequired(ctx context.Context, field graphql.CollectedField, obj *DestinationTypeInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationTypeInfo_statusUpdatesRequired(ctx context.Context, field graphql.CollectedField, obj *nfydest.TypeInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypeInfo_statusUpdatesRequired(ctx, field) if err != nil { return graphql.Null @@ -12141,7 +12142,7 @@ func (ec *executionContext) fieldContext_DestinationTypeInfo_statusUpdatesRequir return fc, nil } -func (ec *executionContext) _DynamicParamConfig_paramID(ctx context.Context, field graphql.CollectedField, obj *DynamicParamConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DynamicParamConfig_paramID(ctx context.Context, field graphql.CollectedField, obj *nfydest.DynamicParamConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DynamicParamConfig_paramID(ctx, field) if err != nil { return graphql.Null @@ -12185,7 +12186,7 @@ func (ec *executionContext) fieldContext_DynamicParamConfig_paramID(_ context.Co return fc, nil } -func (ec *executionContext) _DynamicParamConfig_label(ctx context.Context, field graphql.CollectedField, obj *DynamicParamConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DynamicParamConfig_label(ctx context.Context, field graphql.CollectedField, obj *nfydest.DynamicParamConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DynamicParamConfig_label(ctx, field) if err != nil { return graphql.Null @@ -12229,7 +12230,7 @@ func (ec *executionContext) fieldContext_DynamicParamConfig_label(_ context.Cont return fc, nil } -func (ec *executionContext) _DynamicParamConfig_hint(ctx context.Context, field graphql.CollectedField, obj *DynamicParamConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DynamicParamConfig_hint(ctx context.Context, field graphql.CollectedField, obj *nfydest.DynamicParamConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DynamicParamConfig_hint(ctx, field) if err != nil { return graphql.Null @@ -12273,7 +12274,7 @@ func (ec *executionContext) fieldContext_DynamicParamConfig_hint(_ context.Conte return fc, nil } -func (ec *executionContext) _DynamicParamConfig_hintURL(ctx context.Context, field graphql.CollectedField, obj *DynamicParamConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DynamicParamConfig_hintURL(ctx context.Context, field graphql.CollectedField, obj *nfydest.DynamicParamConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DynamicParamConfig_hintURL(ctx, field) if err != nil { return graphql.Null @@ -12317,7 +12318,7 @@ func (ec *executionContext) fieldContext_DynamicParamConfig_hintURL(_ context.Co return fc, nil } -func (ec *executionContext) _DynamicParamConfig_defaultValue(ctx context.Context, field graphql.CollectedField, obj *DynamicParamConfig) (ret graphql.Marshaler) { +func (ec *executionContext) _DynamicParamConfig_defaultValue(ctx context.Context, field graphql.CollectedField, obj *nfydest.DynamicParamConfig) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DynamicParamConfig_defaultValue(ctx, field) if err != nil { return graphql.Null @@ -23899,9 +23900,9 @@ func (ec *executionContext) _Query_destinationTypes(ctx context.Context, field g } return graphql.Null } - res := resTmp.([]DestinationTypeInfo) + res := resTmp.([]nfydest.TypeInfo) fc.Result = res - return ec.marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationTypeInfoᚄ(ctx, field.Selections, res) + return ec.marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐTypeInfoᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_destinationTypes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -39822,7 +39823,7 @@ func (ec *executionContext) _DestinationDisplayInfoError(ctx context.Context, se var destinationFieldConfigImplementors = []string{"DestinationFieldConfig"} -func (ec *executionContext) _DestinationFieldConfig(ctx context.Context, sel ast.SelectionSet, obj *DestinationFieldConfig) graphql.Marshaler { +func (ec *executionContext) _DestinationFieldConfig(ctx context.Context, sel ast.SelectionSet, obj *nfydest.FieldConfig) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, destinationFieldConfigImplementors) out := graphql.NewFieldSet(fields) @@ -39901,7 +39902,7 @@ func (ec *executionContext) _DestinationFieldConfig(ctx context.Context, sel ast var destinationTypeInfoImplementors = []string{"DestinationTypeInfo"} -func (ec *executionContext) _DestinationTypeInfo(ctx context.Context, sel ast.SelectionSet, obj *DestinationTypeInfo) graphql.Marshaler { +func (ec *executionContext) _DestinationTypeInfo(ctx context.Context, sel ast.SelectionSet, obj *nfydest.TypeInfo) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, destinationTypeInfoImplementors) out := graphql.NewFieldSet(fields) @@ -40005,7 +40006,7 @@ func (ec *executionContext) _DestinationTypeInfo(ctx context.Context, sel ast.Se var dynamicParamConfigImplementors = []string{"DynamicParamConfig"} -func (ec *executionContext) _DynamicParamConfig(ctx context.Context, sel ast.SelectionSet, obj *DynamicParamConfig) graphql.Marshaler { +func (ec *executionContext) _DynamicParamConfig(ctx context.Context, sel ast.SelectionSet, obj *nfydest.DynamicParamConfig) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, dynamicParamConfigImplementors) out := graphql.NewFieldSet(fields) @@ -48149,11 +48150,11 @@ func (ec *executionContext) marshalNDestinationDisplayInfo2ᚖgithubᚗcomᚋtar return ec._DestinationDisplayInfo(ctx, sel, v) } -func (ec *executionContext) marshalNDestinationFieldConfig2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationFieldConfig(ctx context.Context, sel ast.SelectionSet, v DestinationFieldConfig) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationFieldConfig2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐFieldConfig(ctx context.Context, sel ast.SelectionSet, v nfydest.FieldConfig) graphql.Marshaler { return ec._DestinationFieldConfig(ctx, sel, &v) } -func (ec *executionContext) marshalNDestinationFieldConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationFieldConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []DestinationFieldConfig) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationFieldConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐFieldConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []nfydest.FieldConfig) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -48177,7 +48178,7 @@ func (ec *executionContext) marshalNDestinationFieldConfig2ᚕgithubᚗcomᚋtar if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNDestinationFieldConfig2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationFieldConfig(ctx, sel, v[i]) + ret[i] = ec.marshalNDestinationFieldConfig2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐFieldConfig(ctx, sel, v[i]) } if isLen1 { f(i) @@ -48227,11 +48228,11 @@ func (ec *executionContext) marshalNDestinationType2string(ctx context.Context, return res } -func (ec *executionContext) marshalNDestinationTypeInfo2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationTypeInfo(ctx context.Context, sel ast.SelectionSet, v DestinationTypeInfo) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationTypeInfo2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐTypeInfo(ctx context.Context, sel ast.SelectionSet, v nfydest.TypeInfo) graphql.Marshaler { return ec._DestinationTypeInfo(ctx, sel, &v) } -func (ec *executionContext) marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationTypeInfoᚄ(ctx context.Context, sel ast.SelectionSet, v []DestinationTypeInfo) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐTypeInfoᚄ(ctx context.Context, sel ast.SelectionSet, v []nfydest.TypeInfo) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -48255,7 +48256,7 @@ func (ec *executionContext) marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtarget if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNDestinationTypeInfo2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationTypeInfo(ctx, sel, v[i]) + ret[i] = ec.marshalNDestinationTypeInfo2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐTypeInfo(ctx, sel, v[i]) } if isLen1 { f(i) @@ -48275,11 +48276,11 @@ func (ec *executionContext) marshalNDestinationTypeInfo2ᚕgithubᚗcomᚋtarget return ret } -func (ec *executionContext) marshalNDynamicParamConfig2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDynamicParamConfig(ctx context.Context, sel ast.SelectionSet, v DynamicParamConfig) graphql.Marshaler { +func (ec *executionContext) marshalNDynamicParamConfig2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDynamicParamConfig(ctx context.Context, sel ast.SelectionSet, v nfydest.DynamicParamConfig) graphql.Marshaler { return ec._DynamicParamConfig(ctx, sel, &v) } -func (ec *executionContext) marshalNDynamicParamConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDynamicParamConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []DynamicParamConfig) graphql.Marshaler { +func (ec *executionContext) marshalNDynamicParamConfig2ᚕgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDynamicParamConfigᚄ(ctx context.Context, sel ast.SelectionSet, v []nfydest.DynamicParamConfig) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -48303,7 +48304,7 @@ func (ec *executionContext) marshalNDynamicParamConfig2ᚕgithubᚗcomᚋtarget if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNDynamicParamConfig2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDynamicParamConfig(ctx, sel, v[i]) + ret[i] = ec.marshalNDynamicParamConfig2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDynamicParamConfig(ctx, sel, v[i]) } if isLen1 { f(i) diff --git a/graphql2/gqlgen.yml b/graphql2/gqlgen.yml index 58bf10bb7d..843816ad18 100644 --- a/graphql2/gqlgen.yml +++ b/graphql2/gqlgen.yml @@ -138,3 +138,9 @@ models: model: github.com/target/goalert/gadb.UIKRuleV1 KeyConfig: model: github.com/target/goalert/gadb.UIKConfigV1 + DestinationFieldConfig: + model: github.com/target/goalert/notification/nfydest.FieldConfig + DestinationTypeInfo: + model: github.com/target/goalert/notification/nfydest.TypeInfo + DynamicParamConfig: + model: github.com/target/goalert/notification/nfydest.DynamicParamConfig diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 26834bd607..ddd900d285 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -7,6 +7,7 @@ import ( "github.com/nyaruka/phonenumbers" "github.com/target/goalert/config" "github.com/target/goalert/graphql2" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" ) @@ -246,15 +247,15 @@ func (q *Query) DestinationFieldValidate(ctx context.Context, input graphql2.Des return false, validation.NewGenericError("unsupported data type") } -func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]graphql2.DestinationTypeInfo, error) { +func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]nfydest.TypeInfo, error) { cfg := config.FromContext(ctx) - types := []graphql2.DestinationTypeInfo{ + types := []nfydest.TypeInfo{ { Type: destAlert, Name: "Alert", Enabled: true, - IsDynamicAction: true, - DynamicParams: []graphql2.DynamicParamConfig{{ + SupportsSignals: true, + DynamicParams: []nfydest.DynamicParamConfig{{ ParamID: "summary", Label: "Summary", Hint: "Short summary of the alert (used for things like SMS).", @@ -273,13 +274,15 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destTwilioSMS, - Name: "Text Message (SMS)", - Enabled: cfg.Twilio.Enable, - UserDisclaimer: cfg.General.NotificationDisclaimer, - SupportsStatusUpdates: true, - IsContactMethod: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destTwilioSMS, + Name: "Text Message (SMS)", + Enabled: cfg.Twilio.Enable, + UserDisclaimer: cfg.General.NotificationDisclaimer, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldPhoneNumber, Label: "Phone Number", Hint: "Include country code e.g. +1 (USA), +91 (India), +44 (UK)", @@ -290,13 +293,15 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destTwilioVoice, - Name: "Voice Call", - Enabled: cfg.Twilio.Enable, - UserDisclaimer: cfg.General.NotificationDisclaimer, - IsContactMethod: true, - SupportsStatusUpdates: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destTwilioVoice, + Name: "Voice Call", + Enabled: cfg.Twilio.Enable, + UserDisclaimer: cfg.General.NotificationDisclaimer, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldPhoneNumber, Label: "Phone Number", Hint: "Include country code e.g. +1 (USA), +91 (India), +44 (UK)", @@ -307,20 +312,21 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destSMTP, - Name: "Email", - Enabled: cfg.SMTP.Enable, - IsContactMethod: true, - SupportsStatusUpdates: true, - IsDynamicAction: false, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destSMTP, + Name: "Email", + Enabled: cfg.SMTP.Enable, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldEmailAddress, Label: "Email Address", PlaceholderText: "foobar@example.com", InputType: "email", SupportsValidation: true, }}, - DynamicParams: []graphql2.DynamicParamConfig{{ + DynamicParams: []nfydest.DynamicParamConfig{{ ParamID: "subject", Label: "Subject", Hint: "Subject of the email message.", @@ -331,15 +337,16 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destWebhook, - Name: "Webhook", - Enabled: cfg.Webhook.Enable, - IsContactMethod: true, - IsEPTarget: true, - IsSchedOnCallNotify: true, - SupportsStatusUpdates: true, - StatusUpdatesRequired: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destWebhook, + Name: "Webhook", + Enabled: cfg.Webhook.Enable, + SupportsUserVerification: true, + SupportsOnCallNotify: true, + SupportsSignals: true, + SupportsStatusUpdates: true, + SupportsAlertNotifications: true, + StatusUpdatesRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldWebhookURL, Label: "Webhook URL", PlaceholderText: "https://example.com", @@ -348,8 +355,7 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] HintURL: "/docs#webhooks", SupportsValidation: true, }}, - IsDynamicAction: true, - DynamicParams: []graphql2.DynamicParamConfig{ + DynamicParams: []nfydest.DynamicParamConfig{ { ParamID: "body", Label: "Body", @@ -364,13 +370,15 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }, }, { - Type: destSlackDM, - Name: "Slack Message (DM)", - Enabled: cfg.Slack.Enable, - IsContactMethod: true, - SupportsStatusUpdates: true, - StatusUpdatesRequired: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destSlackDM, + Name: "Slack Message (DM)", + Enabled: cfg.Slack.Enable, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + StatusUpdatesRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldSlackUserID, Label: "Slack User", PlaceholderText: "member ID", @@ -380,32 +388,31 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destSlackChan, - Name: "Slack Channel", - Enabled: cfg.Slack.Enable, - IsEPTarget: true, - IsSchedOnCallNotify: true, - IsDynamicAction: true, - SupportsStatusUpdates: true, - StatusUpdatesRequired: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destSlackChan, + Name: "Slack Channel", + Enabled: cfg.Slack.Enable, + SupportsAlertNotifications: true, + SupportsStatusUpdates: true, + SupportsOnCallNotify: true, + StatusUpdatesRequired: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldSlackChanID, Label: "Slack Channel", InputType: "text", SupportsSearch: true, }}, - DynamicParams: []graphql2.DynamicParamConfig{{ + DynamicParams: []nfydest.DynamicParamConfig{{ ParamID: "message", Label: "Message", Hint: "The text of the message to send.", }}, }, { - Type: destSlackUG, - Name: "Update Slack User Group", - Enabled: cfg.Slack.Enable, - IsSchedOnCallNotify: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destSlackUG, + Name: "Update Slack User Group", + Enabled: cfg.Slack.Enable, + SupportsOnCallNotify: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldSlackUGID, Label: "User Group", InputType: "text", @@ -420,11 +427,11 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destRotation, - Name: "Rotation", - Enabled: true, - IsEPTarget: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destRotation, + Name: "Rotation", + Enabled: true, + SupportsAlertNotifications: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldRotationID, Label: "Rotation", InputType: "text", @@ -432,11 +439,11 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destSchedule, - Name: "Schedule", - Enabled: true, - IsEPTarget: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destSchedule, + Name: "Schedule", + Enabled: true, + SupportsAlertNotifications: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldScheduleID, Label: "Schedule", InputType: "text", @@ -444,11 +451,11 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }}, }, { - Type: destUser, - Name: "User", - Enabled: true, - IsEPTarget: true, - RequiredFields: []graphql2.DestinationFieldConfig{{ + Type: destUser, + Name: "User", + Enabled: true, + SupportsAlertNotifications: true, + RequiredFields: []nfydest.FieldConfig{{ FieldID: fieldUserID, Label: "User", InputType: "text", @@ -457,7 +464,7 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }, } - slices.SortStableFunc(types, func(a, b graphql2.DestinationTypeInfo) int { + slices.SortStableFunc(types, func(a, b nfydest.TypeInfo) int { if a.Enabled && !b.Enabled { return -1 } @@ -471,7 +478,7 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] filtered := types[:0] for _, t := range types { - if isDynamicAction != nil && *isDynamicAction != t.IsDynamicAction { + if isDynamicAction != nil && *isDynamicAction != t.IsDynamicAction() { continue } diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index e4e80a3ed7..476e6d1ee5 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -365,27 +365,6 @@ type DestinationDisplayInfoError struct { func (DestinationDisplayInfoError) IsInlineDisplayInfo() {} -type DestinationFieldConfig struct { - // unique ID for the input field - FieldID string `json:"fieldID"` - // user-friendly label (should be singular) - Label string `json:"label"` - // user-friendly helper text for input fields (i.e., "Enter a phone number") - Hint string `json:"hint"` - // URL to link to for more information about the destination type - HintURL string `json:"hintURL"` - // placeholder text to display in input fields (e.g., "Phone Number") - PlaceholderText string `json:"placeholderText"` - // the prefix to use when displaying the destination (e.g., "+" for phone numbers) - Prefix string `json:"prefix"` - // the type of input field (type attribute) to use (e.g., "text" or "tel") - InputType string `json:"inputType"` - // if true, the destination can be selected via search - SupportsSearch bool `json:"supportsSearch"` - // if true, the destination type supports validation - SupportsValidation bool `json:"supportsValidation"` -} - type DestinationFieldSearchInput struct { // the type of destination to search for DestType string `json:"destType"` @@ -410,47 +389,6 @@ type DestinationFieldValidateInput struct { Value string `json:"value"` } -type DestinationTypeInfo struct { - Type string `json:"type"` - Name string `json:"name"` - // URL to an icon to display for the destination type - IconURL string `json:"iconURL"` - // alt text for the icon, should be usable in place of the icon - IconAltText string `json:"iconAltText"` - // if false, the destination type is disabled and cannot be used - Enabled bool `json:"enabled"` - RequiredFields []DestinationFieldConfig `json:"requiredFields"` - // expr parameters that can be used for this destination type - DynamicParams []DynamicParamConfig `json:"dynamicParams"` - // disclaimer text to display when a user is selecting this destination type for a contact method - UserDisclaimer string `json:"userDisclaimer"` - // this destination type can be used as a user contact method - IsContactMethod bool `json:"isContactMethod"` - // this destination type can be used as an escalation policy step action - IsEPTarget bool `json:"isEPTarget"` - // this destination type can be used for schedule on-call notifications - IsSchedOnCallNotify bool `json:"isSchedOnCallNotify"` - // this destination type can be used for dynamic actions - IsDynamicAction bool `json:"isDynamicAction"` - // if true, the destination type supports status updates - SupportsStatusUpdates bool `json:"supportsStatusUpdates"` - // if true, the destination type requires status updates to be enabled - StatusUpdatesRequired bool `json:"statusUpdatesRequired"` -} - -type DynamicParamConfig struct { - // unique ID for the input field - ParamID string `json:"paramID"` - // user-friendly label (should be singular) - Label string `json:"label"` - // user-friendly helper text for input fields (i.e., "Enter a phone number") - Hint string `json:"hint"` - // URL to link to for more information about the destination type - HintURL string `json:"hintURL"` - // default value for the input field - DefaultValue string `json:"defaultValue"` -} - type EscalationPolicyConnection struct { Nodes []escalation.Policy `json:"nodes"` PageInfo *PageInfo `json:"pageInfo"` diff --git a/notification/nfydest/provider.go b/notification/nfydest/provider.go new file mode 100644 index 0000000000..a77ddef17a --- /dev/null +++ b/notification/nfydest/provider.go @@ -0,0 +1,42 @@ +package nfydest + +import "context" + +type Provider interface { + ID() string + TypeInfo(ctx context.Context) (*TypeInfo, error) + + ValidateField(ctx context.Context, fieldID, value string) (ok bool, err error) + DisplayInfo(ctx context.Context, args map[string]string) (*DisplayInfo, error) +} + +type DisplayInfo struct { + Text string + IconURL string + IconAltText string + LinkURL string +} + +type SearchOptions struct { + Search string + Omit []string + Cursor string + Limit int +} + +type SearchResult struct { + HasNextPage bool + Cursor string + Values []FieldValue +} + +type FieldValue struct { + Value string + Label string + IsFavorite bool +} + +type FieldSearcher interface { + SearchField(ctx context.Context, fieldID string, options SearchOptions) (*SearchResult, error) + FieldLabel(ctx context.Context, fieldID, value string) (string, error) +} diff --git a/notification/nfydest/typeinfo.go b/notification/nfydest/typeinfo.go new file mode 100644 index 0000000000..06365880a3 --- /dev/null +++ b/notification/nfydest/typeinfo.go @@ -0,0 +1,61 @@ +package nfydest + +type TypeInfo struct { + Type string + + Name string + IconURL string + IconAltText string + Enabled bool + + RequiredFields []FieldConfig + DynamicParams []DynamicParamConfig + + UserDisclaimer string + + // Message type info + SupportsStatusUpdates bool + SupportsAlertNotifications bool + SupportsUserVerification bool + SupportsOnCallNotify bool + SupportsSignals bool + + UserVerificationRequired bool + StatusUpdatesRequired bool +} + +func (t TypeInfo) IsContactMethod() bool { + return t.SupportsAlertNotifications && t.SupportsUserVerification +} + +func (t TypeInfo) IsEPTarget() bool { + return t.SupportsAlertNotifications && !t.UserVerificationRequired +} + +func (t TypeInfo) IsSchedOnCallNotify() bool { + return t.SupportsOnCallNotify && !t.UserVerificationRequired +} + +func (t TypeInfo) IsDynamicAction() bool { + return t.SupportsSignals && !t.UserVerificationRequired +} + +type FieldConfig struct { + FieldID string + Label string + Hint string + HintURL string + PlaceholderText string + Prefix string + InputType string + SupportsSearch bool + SupportsValidation bool +} + +type DynamicParamConfig struct { + ParamID string + Label string + Hint string + HintURL string + DefaultValue string +} From 008fdbd9df99ba7229b641006ef1f4f851db3644 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 11 Jul 2024 15:20:41 -0500 Subject: [PATCH 02/21] Add provider registry for notification destinations - Implement registry for managing notification providers - Enable provider registration and unique ID enforcement - Support provider-specific field validation and info display - Add error handling for unknown types and unsupported operations --- notification/nfydest/registry.go | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 notification/nfydest/registry.go diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go new file mode 100644 index 0000000000..819bb85962 --- /dev/null +++ b/notification/nfydest/registry.go @@ -0,0 +1,99 @@ +package nfydest + +import ( + "context" + "errors" + "fmt" + + "github.com/target/goalert/gadb" + "github.com/target/goalert/validation" +) + +var ( + ErrUnknownType = validation.NewGenericError("unknown destination type") + ErrUnsupported = errors.New("unsupported operation") +) + +type Registry struct { + providers map[string]Provider + ids []string +} + +func NewRegistry() *Registry { + return &Registry{ + providers: make(map[string]Provider), + } +} + +func (r *Registry) Provider(id string) Provider { return r.providers[id] } + +func (r *Registry) RegisterProvider(ctx context.Context, p Provider) error { + if r.Provider(p.ID()) != nil { + return fmt.Errorf("provider with ID %s already registered", p.ID()) + } + + id := p.ID() + r.providers[id] = p + r.ids = append(r.ids, id) + return nil +} + +func (r *Registry) DisplayInfo(ctx context.Context, d gadb.DestV1) (*DisplayInfo, error) { + p := r.Provider(d.Type) + if p == nil { + return nil, ErrUnknownType + } + + return p.DisplayInfo(ctx, d.Args) +} + +func (r *Registry) ValidateField(ctx context.Context, typeID, fieldID, value string) (bool, error) { + p := r.Provider(typeID) + if p == nil { + return false, ErrUnknownType + } + + return p.ValidateField(ctx, fieldID, value) +} + +func (r *Registry) Types(ctx context.Context) ([]TypeInfo, error) { + var out []TypeInfo + for _, id := range r.ids { + ti, err := r.providers[id].TypeInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get type info for %s: %w", id, err) + } + + out = append(out, *ti) + } + + return out, nil +} + +func (r *Registry) SearchField(ctx context.Context, typeID, fieldID string, options SearchOptions) (*SearchResult, error) { + p := r.Provider(typeID) + if p == nil { + return nil, ErrUnknownType + } + + s, ok := p.(FieldSearcher) + if !ok { + return nil, fmt.Errorf("provider %s does not support field searching: %w", typeID, ErrUnsupported) + } + + return s.SearchField(ctx, fieldID, options) +} + +func (r *Registry) FieldLabel(ctx context.Context, typeID, fieldID, value string) (string, error) { + p := r.Provider(typeID) + if p == nil { + return "", ErrUnknownType + } + + s, ok := p.(FieldSearcher) + if !ok { + return "", fmt.Errorf("provider %s does not support field searching: %w", typeID, ErrUnsupported) + } + + return s.FieldLabel(ctx, fieldID, value) +} From f291155cc20a6b00ef9580b06da0c488792b41fa Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 09:33:16 -0500 Subject: [PATCH 03/21] Refactor DestinationDisplayInfo struct usage - Updated references to DestinationDisplayInfo struct - Changed import paths to use nfydest package - Removed redundant struct definition from models_gen.go - Implemented nfydest.DisplayInfo in related methods and functions --- graphql2/generated.go | 24 ++++++++--------- graphql2/gqlgen.yml | 2 ++ graphql2/graphqlapp/destinationdisplayinfo.go | 27 ++++++++++--------- graphql2/models_gen.go | 14 ---------- notification/nfydest/provider.go | 2 ++ 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/graphql2/generated.go b/graphql2/generated.go index 9a370ad510..82aaa24dd1 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -1040,7 +1040,7 @@ type QueryResolver interface { DestinationFieldValidate(ctx context.Context, input DestinationFieldValidateInput) (bool, error) DestinationFieldSearch(ctx context.Context, input DestinationFieldSearchInput) (*FieldSearchConnection, error) DestinationFieldValueName(ctx context.Context, input DestinationFieldValidateInput) (string, error) - DestinationDisplayInfo(ctx context.Context, input gadb.DestV1) (*DestinationDisplayInfo, error) + DestinationDisplayInfo(ctx context.Context, input gadb.DestV1) (*nfydest.DisplayInfo, error) Expr(ctx context.Context) (*Expr, error) GqlAPIKeys(ctx context.Context) ([]GQLAPIKey, error) ActionInputValidate(ctx context.Context, input gadb.UIKActionV1) (bool, error) @@ -10878,7 +10878,7 @@ func (ec *executionContext) fieldContext_Destination_displayInfo(_ context.Conte return fc, nil } -func (ec *executionContext) _DestinationDisplayInfo_text(ctx context.Context, field graphql.CollectedField, obj *DestinationDisplayInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationDisplayInfo_text(ctx context.Context, field graphql.CollectedField, obj *nfydest.DisplayInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationDisplayInfo_text(ctx, field) if err != nil { return graphql.Null @@ -10922,7 +10922,7 @@ func (ec *executionContext) fieldContext_DestinationDisplayInfo_text(_ context.C return fc, nil } -func (ec *executionContext) _DestinationDisplayInfo_iconURL(ctx context.Context, field graphql.CollectedField, obj *DestinationDisplayInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationDisplayInfo_iconURL(ctx context.Context, field graphql.CollectedField, obj *nfydest.DisplayInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationDisplayInfo_iconURL(ctx, field) if err != nil { return graphql.Null @@ -10966,7 +10966,7 @@ func (ec *executionContext) fieldContext_DestinationDisplayInfo_iconURL(_ contex return fc, nil } -func (ec *executionContext) _DestinationDisplayInfo_iconAltText(ctx context.Context, field graphql.CollectedField, obj *DestinationDisplayInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationDisplayInfo_iconAltText(ctx context.Context, field graphql.CollectedField, obj *nfydest.DisplayInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationDisplayInfo_iconAltText(ctx, field) if err != nil { return graphql.Null @@ -11010,7 +11010,7 @@ func (ec *executionContext) fieldContext_DestinationDisplayInfo_iconAltText(_ co return fc, nil } -func (ec *executionContext) _DestinationDisplayInfo_linkURL(ctx context.Context, field graphql.CollectedField, obj *DestinationDisplayInfo) (ret graphql.Marshaler) { +func (ec *executionContext) _DestinationDisplayInfo_linkURL(ctx context.Context, field graphql.CollectedField, obj *nfydest.DisplayInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationDisplayInfo_linkURL(ctx, field) if err != nil { return graphql.Null @@ -24156,9 +24156,9 @@ func (ec *executionContext) _Query_destinationDisplayInfo(ctx context.Context, f } return graphql.Null } - res := resTmp.(*DestinationDisplayInfo) + res := resTmp.(*nfydest.DisplayInfo) fc.Result = res - return ec.marshalNDestinationDisplayInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationDisplayInfo(ctx, field.Selections, res) + return ec.marshalNDestinationDisplayInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDisplayInfo(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Query_destinationDisplayInfo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -38052,9 +38052,9 @@ func (ec *executionContext) _InlineDisplayInfo(ctx context.Context, sel ast.Sele switch obj := (obj).(type) { case nil: return graphql.Null - case DestinationDisplayInfo: + case nfydest.DisplayInfo: return ec._DestinationDisplayInfo(ctx, sel, &obj) - case *DestinationDisplayInfo: + case *nfydest.DisplayInfo: if obj == nil { return graphql.Null } @@ -39730,7 +39730,7 @@ func (ec *executionContext) _Destination(ctx context.Context, sel ast.SelectionS var destinationDisplayInfoImplementors = []string{"DestinationDisplayInfo", "InlineDisplayInfo"} -func (ec *executionContext) _DestinationDisplayInfo(ctx context.Context, sel ast.SelectionSet, obj *DestinationDisplayInfo) graphql.Marshaler { +func (ec *executionContext) _DestinationDisplayInfo(ctx context.Context, sel ast.SelectionSet, obj *nfydest.DisplayInfo) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, destinationDisplayInfoImplementors) out := graphql.NewFieldSet(fields) @@ -48136,11 +48136,11 @@ func (ec *executionContext) marshalNDestination2ᚖgithubᚗcomᚋtargetᚋgoale return ec._Destination(ctx, sel, v) } -func (ec *executionContext) marshalNDestinationDisplayInfo2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationDisplayInfo(ctx context.Context, sel ast.SelectionSet, v DestinationDisplayInfo) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationDisplayInfo2githubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDisplayInfo(ctx context.Context, sel ast.SelectionSet, v nfydest.DisplayInfo) graphql.Marshaler { return ec._DestinationDisplayInfo(ctx, sel, &v) } -func (ec *executionContext) marshalNDestinationDisplayInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐDestinationDisplayInfo(ctx context.Context, sel ast.SelectionSet, v *DestinationDisplayInfo) graphql.Marshaler { +func (ec *executionContext) marshalNDestinationDisplayInfo2ᚖgithubᚗcomᚋtargetᚋgoalertᚋnotificationᚋnfydestᚐDisplayInfo(ctx context.Context, sel ast.SelectionSet, v *nfydest.DisplayInfo) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") diff --git a/graphql2/gqlgen.yml b/graphql2/gqlgen.yml index 843816ad18..340b9ccd46 100644 --- a/graphql2/gqlgen.yml +++ b/graphql2/gqlgen.yml @@ -144,3 +144,5 @@ models: model: github.com/target/goalert/notification/nfydest.TypeInfo DynamicParamConfig: model: github.com/target/goalert/notification/nfydest.DynamicParamConfig + DestinationDisplayInfo: + model: github.com/target/goalert/notification/nfydest.DisplayInfo diff --git a/graphql2/graphqlapp/destinationdisplayinfo.go b/graphql2/graphqlapp/destinationdisplayinfo.go index 33a4123d91..4555bf4b1d 100644 --- a/graphql2/graphqlapp/destinationdisplayinfo.go +++ b/graphql2/graphqlapp/destinationdisplayinfo.go @@ -9,6 +9,7 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/gadb" "github.com/target/goalert/graphql2" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/util/errutil" "github.com/target/goalert/util/log" "github.com/target/goalert/validation" @@ -56,11 +57,11 @@ func (a *Destination) DisplayInfo(ctx context.Context, obj *gadb.DestV1) (graphq return info, nil } -func (a *Query) DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1) (*graphql2.DestinationDisplayInfo, error) { +func (a *Query) DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1) (*nfydest.DisplayInfo, error) { return a._DestinationDisplayInfo(ctx, dest, false) } -func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, skipValidation bool) (*graphql2.DestinationDisplayInfo, error) { +func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, skipValidation bool) (*nfydest.DisplayInfo, error) { app := (*App)(a) cfg := config.FromContext(ctx) if !skipValidation { @@ -70,7 +71,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s } switch dest.Type { case destAlert: - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://alert", IconAltText: "Alert", Text: "Create new alert", @@ -81,7 +82,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s return nil, validation.WrapError(err) } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://phone-text", IconAltText: "Text Message", Text: phonenumbers.Format(n, phonenumbers.INTERNATIONAL), @@ -91,7 +92,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, validation.WrapError(err) } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://phone-voice", IconAltText: "Voice Call", Text: phonenumbers.Format(n, phonenumbers.INTERNATIONAL), @@ -101,7 +102,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, validation.WrapError(err) } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://email", IconAltText: "Email", Text: e.Address, @@ -111,7 +112,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, err } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://rotation", IconAltText: "Rotation", LinkURL: cfg.CallbackURL("/rotations/" + r.ID), @@ -122,7 +123,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, err } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://schedule", IconAltText: "Schedule", LinkURL: cfg.CallbackURL("/schedules/" + s.ID), @@ -133,7 +134,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, err } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: cfg.CallbackURL("/api/v2/user-avatar/" + u.ID), IconAltText: "User", LinkURL: cfg.CallbackURL("/users/" + u.ID), @@ -145,7 +146,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if err != nil { return nil, validation.WrapError(err) } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: "builtin://webhook", IconAltText: "Webhook", Text: u.Hostname(), @@ -165,7 +166,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s team.IconURL = "builtin://slack" } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: team.IconURL, IconAltText: team.Name, LinkURL: team.UserLink(u.ID), @@ -185,7 +186,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if team.IconURL == "" { team.IconURL = "builtin://slack" } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: team.IconURL, IconAltText: team.Name, LinkURL: team.ChannelLink(ch.ID), @@ -206,7 +207,7 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s if team.IconURL == "" { team.IconURL = "builtin://slack" } - return &graphql2.DestinationDisplayInfo{ + return &nfydest.DisplayInfo{ IconURL: team.IconURL, IconAltText: team.Name, Text: ug.Handle, diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 476e6d1ee5..c1c251fe9f 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -344,20 +344,6 @@ type DebugSendSMSInput struct { Body string `json:"body"` } -// DestinationDisplayInfo provides information for displaying a destination. -type DestinationDisplayInfo struct { - // user-friendly text to display for this destination - Text string `json:"text"` - // URL to an icon to display for this destination - IconURL string `json:"iconURL"` - // alt text for the icon, should be human-readable and usable in place of the icon - IconAltText string `json:"iconAltText"` - // URL to link to for more information about this destination - LinkURL string `json:"linkURL"` -} - -func (DestinationDisplayInfo) IsInlineDisplayInfo() {} - type DestinationDisplayInfoError struct { // error message to display when the display info cannot be retrieved Error string `json:"error"` diff --git a/notification/nfydest/provider.go b/notification/nfydest/provider.go index a77ddef17a..751e715d3f 100644 --- a/notification/nfydest/provider.go +++ b/notification/nfydest/provider.go @@ -17,6 +17,8 @@ type DisplayInfo struct { LinkURL string } +func (DisplayInfo) IsInlineDisplayInfo() {} + type SearchOptions struct { Search string Omit []string From 4e8277ee2d4057f45aeb0ce716fc09a692cb81c1 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 10:10:48 -0500 Subject: [PATCH 04/21] Integrate nfydest for enhanced destination handling - Added nfydest registry for improved destination management - Implemented Slack channel and DM destination types - Refactored destination fields handling to leverage nfydest - Introduced support for searching and validating destination fields via nfydest --- graphql2/graphqlapp/app.go | 3 + graphql2/graphqlapp/destinationtypes.go | 44 +++++++++- notification/nfydest/provider.go | 24 ----- notification/nfydest/search.go | 32 +++++++ notification/slack/nfydest.go | 13 +++ notification/slack/nfydestchannel.go | 112 ++++++++++++++++++++++++ notification/slack/nfydestdm.go | 75 ++++++++++++++++ 7 files changed, 276 insertions(+), 27 deletions(-) create mode 100644 notification/nfydest/search.go create mode 100644 notification/slack/nfydest.go create mode 100644 notification/slack/nfydestchannel.go create mode 100644 notification/slack/nfydestdm.go diff --git a/graphql2/graphqlapp/app.go b/graphql2/graphqlapp/app.go index fa8b80e0c6..ef3199af23 100644 --- a/graphql2/graphqlapp/app.go +++ b/graphql2/graphqlapp/app.go @@ -31,6 +31,7 @@ import ( "github.com/target/goalert/limit" "github.com/target/goalert/notice" "github.com/target/goalert/notification" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/notification/slack" "github.com/target/goalert/notification/twilio" "github.com/target/goalert/notificationchannel" @@ -94,6 +95,8 @@ type App struct { SWO *swo.Manager + DestReg *nfydest.Registry + FormatDestFunc func(context.Context, notification.DestType, string) string } diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index ddd900d285..4caf595fd5 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -80,7 +80,7 @@ func (q *Query) DestinationFieldValueName(ctx context.Context, input graphql2.De return u.Name, nil } - return "", validation.NewGenericError("unsupported fieldID") + return q.DestReg.FieldLabel(ctx, input.DestType, input.FieldID, input.Value) } func (q *Query) DestinationFieldSearch(ctx context.Context, input graphql2.DestinationFieldSearchInput) (*graphql2.FieldSearchConnection, error) { @@ -215,7 +215,39 @@ func (q *Query) DestinationFieldSearch(ctx context.Context, input graphql2.Desti }, nil } - return nil, validation.NewGenericError("unsupported fieldID") + var opts nfydest.SearchOptions + opts.Omit = input.Omit + if input.First != nil { + opts.Limit = *input.First + } + if input.After != nil { + opts.Cursor = *input.After + } + if input.Search != nil { + opts.Search = *input.Search + } + + res, err := q.DestReg.SearchField(ctx, input.DestType, input.FieldID, opts) + if err != nil { + return nil, err + } + var nodes []graphql2.FieldSearchResult + for _, v := range res.Values { + nodes = append(nodes, graphql2.FieldSearchResult{ + FieldID: input.FieldID, + Value: v.Value, + Label: v.Label, + IsFavorite: v.IsFavorite, + }) + } + + return &graphql2.FieldSearchConnection{ + Nodes: nodes, + PageInfo: &graphql2.PageInfo{ + HasNextPage: res.HasNextPage, + EndCursor: &res.Cursor, + }, + }, nil } func (q *Query) DestinationFieldValidate(ctx context.Context, input graphql2.DestinationFieldValidateInput) (bool, error) { @@ -244,7 +276,7 @@ func (q *Query) DestinationFieldValidate(ctx context.Context, input graphql2.Des return err == nil, nil } - return false, validation.NewGenericError("unsupported data type") + return q.DestReg.ValidateField(ctx, input.DestType, input.FieldID, input.Value) } func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]nfydest.TypeInfo, error) { @@ -464,6 +496,12 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }, } + fromReg, err := q.DestReg.Types(ctx) + if err != nil { + return nil, err + } + types = append(types, fromReg...) + slices.SortStableFunc(types, func(a, b nfydest.TypeInfo) int { if a.Enabled && !b.Enabled { return -1 diff --git a/notification/nfydest/provider.go b/notification/nfydest/provider.go index 751e715d3f..824b561645 100644 --- a/notification/nfydest/provider.go +++ b/notification/nfydest/provider.go @@ -18,27 +18,3 @@ type DisplayInfo struct { } func (DisplayInfo) IsInlineDisplayInfo() {} - -type SearchOptions struct { - Search string - Omit []string - Cursor string - Limit int -} - -type SearchResult struct { - HasNextPage bool - Cursor string - Values []FieldValue -} - -type FieldValue struct { - Value string - Label string - IsFavorite bool -} - -type FieldSearcher interface { - SearchField(ctx context.Context, fieldID string, options SearchOptions) (*SearchResult, error) - FieldLabel(ctx context.Context, fieldID, value string) (string, error) -} diff --git a/notification/nfydest/search.go b/notification/nfydest/search.go new file mode 100644 index 0000000000..c88b333f25 --- /dev/null +++ b/notification/nfydest/search.go @@ -0,0 +1,32 @@ +package nfydest + +import "context" + +type FieldSearcher interface { + SearchField(ctx context.Context, fieldID string, options SearchOptions) (*SearchResult, error) + FieldLabel(ctx context.Context, fieldID, value string) (string, error) +} + +type SearchResult struct { + HasNextPage bool + Cursor string + Values []FieldValue +} + +type FieldValue struct { + Value string + Label string + IsFavorite bool +} + +type SearchOptions struct { + Search string + Omit []string + Cursor string + Limit int +} + +// SearchByList allows returning a SearchResult from a list of items, handling pagination and filtering. +func SearchByList[t any](items []t, opts SearchOptions, fn func(t) FieldValue) (*SearchResult, error) { + return nil, nil +} diff --git a/notification/slack/nfydest.go b/notification/slack/nfydest.go new file mode 100644 index 0000000000..404a655273 --- /dev/null +++ b/notification/slack/nfydest.go @@ -0,0 +1,13 @@ +package slack + +const ( + DestTypeSlackDirectMessage = "builtin-slack-dm" + DestTypeSlackChannel = "builtin-slack-channel" + DestTypeSlackUsergroup = "builtin-slack-usergroup" + + FieldSlackUserID = "slack_user_id" + FieldSlackChannelID = "slack_channel_id" + FieldSlackUsergroupID = "slack_usergroup_id" + + FallbackIconURL = "builtin://slack" +) diff --git a/notification/slack/nfydestchannel.go b/notification/slack/nfydestchannel.go new file mode 100644 index 0000000000..e854708efb --- /dev/null +++ b/notification/slack/nfydestchannel.go @@ -0,0 +1,112 @@ +package slack + +import ( + "context" + + "github.com/target/goalert/config" + "github.com/target/goalert/notification/nfydest" + "github.com/target/goalert/validation" +) + +var ( + _ nfydest.Provider = (*ChannelSender)(nil) + _ nfydest.FieldSearcher = (*ChannelSender)(nil) +) + +func (s *ChannelSender) ID() string { return DestTypeSlackChannel } +func (s *ChannelSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { + cfg := config.FromContext(ctx) + return &nfydest.TypeInfo{ + Type: DestTypeSlackChannel, + Name: "Slack Channel", + Enabled: cfg.Slack.Enable, + SupportsAlertNotifications: true, + SupportsStatusUpdates: true, + SupportsOnCallNotify: true, + StatusUpdatesRequired: true, + SupportsSignals: true, + RequiredFields: []nfydest.FieldConfig{{ + FieldID: FieldSlackChannelID, + Label: "Slack Channel", + InputType: "text", + SupportsSearch: true, + }}, + DynamicParams: []nfydest.DynamicParamConfig{{ + ParamID: "message", + Label: "Message", + Hint: "The text of the message to send.", + }}, + }, nil +} + +func (s *ChannelSender) ValidateField(ctx context.Context, fieldID, value string) (bool, error) { + switch fieldID { + case FieldSlackChannelID: + err := s.ValidateChannel(ctx, value) + if validation.IsValidationError(err) { + return false, nil + } + + return err == nil, err + } + + return false, validation.NewGenericError("unknown field ID") +} + +func (s *ChannelSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { + if args == nil { + args = make(map[string]string) + } + + ch, err := s.Channel(ctx, args[FieldSlackChannelID]) + if err != nil { + return nil, err + } + + team, err := s.Team(ctx, ch.TeamID) + if err != nil { + return nil, err + } + + if team.IconURL == "" { + team.IconURL = FallbackIconURL + } + return &nfydest.DisplayInfo{ + IconURL: team.IconURL, + IconAltText: team.Name, + LinkURL: team.ChannelLink(ch.ID), + Text: ch.Name, + }, nil +} + +func (s *ChannelSender) SearchField(ctx context.Context, fieldID string, options nfydest.SearchOptions) (*nfydest.SearchResult, error) { + switch fieldID { + case FieldSlackChannelID: + ch, err := s.ListChannels(ctx) + if err != nil { + return nil, err + } + return nfydest.SearchByList(ch, options, func(ch Channel) nfydest.FieldValue { + return nfydest.FieldValue{ + Label: ch.Name, + Value: ch.ID, + } + }) + } + + return nil, validation.NewGenericError("unsupported field ID") +} + +func (s *ChannelSender) FieldLabel(ctx context.Context, fieldID, value string) (string, error) { + switch fieldID { + case FieldSlackChannelID: + ch, err := s.Channel(ctx, value) + if err != nil { + return "", err + } + + return ch.Name, nil + } + + return "", validation.NewGenericError("unsupported field ID") +} diff --git a/notification/slack/nfydestdm.go b/notification/slack/nfydestdm.go new file mode 100644 index 0000000000..af1a4ab504 --- /dev/null +++ b/notification/slack/nfydestdm.go @@ -0,0 +1,75 @@ +package slack + +import ( + "context" + + "github.com/target/goalert/config" + "github.com/target/goalert/notification/nfydest" + "github.com/target/goalert/validation" +) + +var _ nfydest.Provider = (*DMSender)(nil) + +func (dm *DMSender) ID() string { return DestTypeSlackDirectMessage } +func (dm *DMSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { + cfg := config.FromContext(ctx) + + return &nfydest.TypeInfo{ + Type: DestTypeSlackDirectMessage, + Name: "Slack Message (DM)", + Enabled: cfg.Slack.Enable, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + StatusUpdatesRequired: true, + RequiredFields: []nfydest.FieldConfig{{ + FieldID: FieldSlackUserID, + Label: "Slack User", + PlaceholderText: "member ID", + InputType: "text", + // supportsSearch: true, // TODO: implement search select functionality for users + Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, + }}, + }, nil +} + +func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) (ok bool, err error) { + switch fieldID { + case FieldSlackUserID: + _, err := dm.User(ctx, value) + if err != nil { + return false, err + } + return true, nil + } + + return false, validation.NewGenericError("unknown field ID") +} + +func (dm *DMSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { + if args == nil { + args = make(map[string]string) + } + + u, err := dm.User(ctx, args[FieldSlackUserID]) + if err != nil { + return nil, err + } + + team, err := dm.Team(ctx, u.TeamID) + if err != nil { + return nil, err + } + + if team.IconURL == "" { + team.IconURL = FallbackIconURL + } + + return &nfydest.DisplayInfo{ + IconURL: team.IconURL, + IconAltText: team.Name, + LinkURL: team.UserLink(u.ID), + Text: u.Name, + }, nil +} From bdf89e2ca3bc73870e12f958141caf273d03b45e Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 10:27:22 -0500 Subject: [PATCH 05/21] Enhance SearchByList for better pagination and filtering - Implement default limit, sorting, and case-insensitive search - Add handling for pagination, filtering by cursor, and omit options --- notification/nfydest/search.go | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/notification/nfydest/search.go b/notification/nfydest/search.go index c88b333f25..43ff715da7 100644 --- a/notification/nfydest/search.go +++ b/notification/nfydest/search.go @@ -1,6 +1,11 @@ package nfydest -import "context" +import ( + "context" + "slices" + "sort" + "strings" +) type FieldSearcher interface { SearchField(ctx context.Context, fieldID string, options SearchOptions) (*SearchResult, error) @@ -27,6 +32,55 @@ type SearchOptions struct { } // SearchByList allows returning a SearchResult from a list of items, handling pagination and filtering. -func SearchByList[t any](items []t, opts SearchOptions, fn func(t) FieldValue) (*SearchResult, error) { - return nil, nil +func SearchByList[t any](_items []t, searchOpts SearchOptions, fn func(t) FieldValue) (*SearchResult, error) { + if searchOpts.Limit <= 0 { + searchOpts.Limit = 15 // Default limit. + } + + items := make([]FieldValue, len(_items)) + for i, item := range _items { + items[i] = fn(item) + } + // Sort by name, case-insensitive, then sensitive. + sort.Slice(items, func(i, j int) bool { + iLabel, jLabel := strings.ToLower(items[i].Label), strings.ToLower(items[j].Label) + + if iLabel != jLabel { + return iLabel < jLabel + } + return items[i].Label < items[j].Label + }) + + // No DB search, so we manually filter for the cursor and search strings. + searchOpts.Search = strings.ToLower(searchOpts.Search) + filtered := items[:0] + for _, item := range items { + lowerName := strings.ToLower(item.Label) + if !strings.Contains(lowerName, searchOpts.Search) { + continue + } + if searchOpts.Cursor != "" && item.Label <= searchOpts.Cursor { + continue + } + if slices.Contains(searchOpts.Omit, item.Value) { + continue + } + filtered = append(filtered, item) + } + items = filtered + + hasNextPage := len(items) > searchOpts.Limit + if hasNextPage { + items = items[:searchOpts.Limit] + } + var cursor string + if len(items) > 0 { + cursor = items[len(items)-1].Label + } + + return &SearchResult{ + HasNextPage: hasNextPage, + Cursor: cursor, + Values: items, + }, nil } From e6df04edd4d8b6dec9c3446406634b8b32a99218 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 10:30:38 -0500 Subject: [PATCH 06/21] Enable new notification provider registry - Added DestRegistry to initialize stores and register providers - Removed unused Slack DM sender implementation - Changed registry behavior to panic on duplicate providers --- app/app.go | 2 + app/initstores.go | 3 ++ app/startup.go | 2 + notification/nfydest/registry.go | 5 +-- notification/slack/nfydestdm.go | 75 -------------------------------- 5 files changed, 9 insertions(+), 78 deletions(-) delete mode 100644 notification/slack/nfydestdm.go diff --git a/app/app.go b/app/app.go index 2dcad6abd3..3c512399ff 100644 --- a/app/app.go +++ b/app/app.go @@ -31,6 +31,7 @@ import ( "github.com/target/goalert/limit" "github.com/target/goalert/notice" "github.com/target/goalert/notification" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/notification/slack" "github.com/target/goalert/notification/twilio" "github.com/target/goalert/notificationchannel" @@ -105,6 +106,7 @@ type App struct { NotificationStore *notification.Store ScheduleStore *schedule.Store RotationStore *rotation.Store + DestRegistry *nfydest.Registry CalSubStore *calsub.Store OverrideStore *override.Store diff --git a/app/initstores.go b/app/initstores.go index 239db3fe8e..1927b9c9eb 100644 --- a/app/initstores.go +++ b/app/initstores.go @@ -22,6 +22,7 @@ import ( "github.com/target/goalert/limit" "github.com/target/goalert/notice" "github.com/target/goalert/notification" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/notification/slack" "github.com/target/goalert/notificationchannel" "github.com/target/goalert/oncall" @@ -302,5 +303,7 @@ func (app *App) initStores(ctx context.Context) error { app.UIKHandler = uik.NewHandler(app.db, app.IntegrationKeyStore, app.AlertStore) + app.DestRegistry = nfydest.NewRegistry() + return nil } diff --git a/app/startup.go b/app/startup.go index 957bd5db86..53421b1448 100644 --- a/app/startup.go +++ b/app/startup.go @@ -73,6 +73,8 @@ func (app *App) startup(ctx context.Context) error { app.notificationManager.RegisterSender(notification.DestTypeUserWebhook, "webhook-user", webhook.NewSender(ctx)) app.notificationManager.RegisterSender(notification.DestTypeChanWebhook, "webhook-channel", webhook.NewSender(ctx)) + app.DestRegistry.RegisterProvider(ctx, app.slackChan) + app.initStartup(ctx, "Startup.Engine", app.initEngine) app.initStartup(ctx, "Startup.Auth", app.initAuth) app.initStartup(ctx, "Startup.GraphQL", app.initGraphQL) diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go index 819bb85962..25359842cb 100644 --- a/notification/nfydest/registry.go +++ b/notification/nfydest/registry.go @@ -27,15 +27,14 @@ func NewRegistry() *Registry { func (r *Registry) Provider(id string) Provider { return r.providers[id] } -func (r *Registry) RegisterProvider(ctx context.Context, p Provider) error { +func (r *Registry) RegisterProvider(ctx context.Context, p Provider) { if r.Provider(p.ID()) != nil { - return fmt.Errorf("provider with ID %s already registered", p.ID()) + panic(fmt.Sprintf("provider with ID %s already registered", p.ID())) } id := p.ID() r.providers[id] = p r.ids = append(r.ids, id) - return nil } func (r *Registry) DisplayInfo(ctx context.Context, d gadb.DestV1) (*DisplayInfo, error) { diff --git a/notification/slack/nfydestdm.go b/notification/slack/nfydestdm.go deleted file mode 100644 index af1a4ab504..0000000000 --- a/notification/slack/nfydestdm.go +++ /dev/null @@ -1,75 +0,0 @@ -package slack - -import ( - "context" - - "github.com/target/goalert/config" - "github.com/target/goalert/notification/nfydest" - "github.com/target/goalert/validation" -) - -var _ nfydest.Provider = (*DMSender)(nil) - -func (dm *DMSender) ID() string { return DestTypeSlackDirectMessage } -func (dm *DMSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { - cfg := config.FromContext(ctx) - - return &nfydest.TypeInfo{ - Type: DestTypeSlackDirectMessage, - Name: "Slack Message (DM)", - Enabled: cfg.Slack.Enable, - SupportsAlertNotifications: true, - SupportsUserVerification: true, - SupportsStatusUpdates: true, - UserVerificationRequired: true, - StatusUpdatesRequired: true, - RequiredFields: []nfydest.FieldConfig{{ - FieldID: FieldSlackUserID, - Label: "Slack User", - PlaceholderText: "member ID", - InputType: "text", - // supportsSearch: true, // TODO: implement search select functionality for users - Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, - }}, - }, nil -} - -func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) (ok bool, err error) { - switch fieldID { - case FieldSlackUserID: - _, err := dm.User(ctx, value) - if err != nil { - return false, err - } - return true, nil - } - - return false, validation.NewGenericError("unknown field ID") -} - -func (dm *DMSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { - if args == nil { - args = make(map[string]string) - } - - u, err := dm.User(ctx, args[FieldSlackUserID]) - if err != nil { - return nil, err - } - - team, err := dm.Team(ctx, u.TeamID) - if err != nil { - return nil, err - } - - if team.IconURL == "" { - team.IconURL = FallbackIconURL - } - - return &nfydest.DisplayInfo{ - IconURL: team.IconURL, - IconAltText: team.Name, - LinkURL: team.UserLink(u.ID), - Text: u.Name, - }, nil -} From 55844ee2d63a3bb937d3fcd81f6cc69b20e3c841 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 10:37:31 -0500 Subject: [PATCH 07/21] Refactor Slack channel constants and field IDs - Use shared package for Slack constants and field IDs - Remove redundant Slack channel handling code - Simplify destination display logic with registry delegate --- graphql2/graphqlapp/compat.go | 19 ++++--- graphql2/graphqlapp/destinationdisplayinfo.go | 22 +------- graphql2/graphqlapp/destinationtypes.go | 56 +------------------ graphql2/graphqlapp/destinationvalidation.go | 11 ++-- 4 files changed, 19 insertions(+), 89 deletions(-) diff --git a/graphql2/graphqlapp/compat.go b/graphql2/graphqlapp/compat.go index 079ecbe95d..4873a25ec8 100644 --- a/graphql2/graphqlapp/compat.go +++ b/graphql2/graphqlapp/compat.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/target/goalert/assignment" "github.com/target/goalert/gadb" + "github.com/target/goalert/notification/slack" "github.com/target/goalert/notificationchannel" "github.com/target/goalert/user/contactmethod" ) @@ -37,8 +38,8 @@ func CompatTargetToDest(tgt assignment.Target) (gadb.DestV1, error) { }, nil case assignment.TargetTypeSlackChannel: return gadb.DestV1{ - Type: destSlackChan, - Args: map[string]string{fieldSlackChanID: tgt.TargetID()}, + Type: slack.DestTypeSlackChannel, + Args: map[string]string{slack.FieldSlackChannelID: tgt.TargetID()}, }, nil } @@ -55,8 +56,8 @@ func (a *App) CompatNCToDest(ctx context.Context, ncID uuid.UUID) (*gadb.DestV1, switch nc.Type { case notificationchannel.TypeSlackChan: return &gadb.DestV1{ - Type: destSlackChan, - Args: map[string]string{fieldSlackChanID: nc.Value}, + Type: slack.DestTypeSlackChannel, + Args: map[string]string{slack.FieldSlackChannelID: nc.Value}, }, nil case notificationchannel.TypeSlackUG: ugID, chanID, ok := strings.Cut(nc.Value, ":") @@ -67,8 +68,8 @@ func (a *App) CompatNCToDest(ctx context.Context, ncID uuid.UUID) (*gadb.DestV1, return &gadb.DestV1{ Type: destSlackUG, Args: map[string]string{ - fieldSlackUGID: ugID, - fieldSlackChanID: chanID, + fieldSlackUGID: ugID, + slack.FieldSlackChannelID: chanID, }, }, nil case notificationchannel.TypeWebhook: @@ -118,15 +119,15 @@ func CompatDestToTarget(d gadb.DestV1) (assignment.RawTarget, error) { Type: assignment.TargetTypeSchedule, ID: d.Arg(fieldScheduleID), }, nil - case destSlackChan: + case slack.DestTypeSlackChannel: return assignment.RawTarget{ Type: assignment.TargetTypeSlackChannel, - ID: d.Arg(fieldSlackChanID), + ID: d.Arg(slack.FieldSlackChannelID), }, nil case destSlackUG: return assignment.RawTarget{ Type: assignment.TargetTypeSlackUserGroup, - ID: d.Arg(fieldSlackUGID) + ":" + d.Arg(fieldSlackChanID), + ID: d.Arg(fieldSlackUGID) + ":" + d.Arg(slack.FieldSlackChannelID), }, nil case destWebhook: return assignment.RawTarget{ diff --git a/graphql2/graphqlapp/destinationdisplayinfo.go b/graphql2/graphqlapp/destinationdisplayinfo.go index 4555bf4b1d..068e8734d4 100644 --- a/graphql2/graphqlapp/destinationdisplayinfo.go +++ b/graphql2/graphqlapp/destinationdisplayinfo.go @@ -172,26 +172,6 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s LinkURL: team.UserLink(u.ID), Text: u.Name, }, nil - case destSlackChan: - ch, err := app.SlackStore.Channel(ctx, dest.Arg(fieldSlackChanID)) - if err != nil { - return nil, err - } - - team, err := app.SlackStore.Team(ctx, ch.TeamID) - if err != nil { - return nil, err - } - - if team.IconURL == "" { - team.IconURL = "builtin://slack" - } - return &nfydest.DisplayInfo{ - IconURL: team.IconURL, - IconAltText: team.Name, - LinkURL: team.ChannelLink(ch.ID), - Text: ch.Name, - }, nil case destSlackUG: ug, err := app.SlackStore.UserGroup(ctx, dest.Arg(fieldSlackUGID)) @@ -214,5 +194,5 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s }, nil } - return nil, validation.NewGenericError("unsupported data type") + return app.DestReg.DisplayInfo(ctx, dest) } diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 4caf595fd5..63dc9dcb69 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -8,6 +8,7 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/graphql2" "github.com/target/goalert/notification/nfydest" + "github.com/target/goalert/notification/slack" "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" ) @@ -19,7 +20,6 @@ const ( destSMTP = "builtin-smtp-email" destWebhook = "builtin-webhook" destSlackDM = "builtin-slack-dm" - destSlackChan = "builtin-slack-channel" destSlackUG = "builtin-slack-usergroup" destUser = "builtin-user" destRotation = "builtin-rotation" @@ -30,7 +30,6 @@ const ( fieldEmailAddress = "email_address" fieldWebhookURL = "webhook_url" fieldSlackUserID = "slack_user_id" - fieldSlackChanID = "slack_channel_id" fieldSlackUGID = "slack_usergroup_id" fieldUserID = "user_id" fieldRotationID = "rotation_id" @@ -44,13 +43,6 @@ type ( func (q *Query) DestinationFieldValueName(ctx context.Context, input graphql2.DestinationFieldValidateInput) (string, error) { switch input.FieldID { - case fieldSlackChanID: - ch, err := q.SlackChannel(ctx, input.Value) - if err != nil { - return "", err - } - - return ch.Name, nil case fieldSlackUGID: ug, err := q.SlackUserGroup(ctx, input.Value) if err != nil { @@ -87,30 +79,6 @@ func (q *Query) DestinationFieldSearch(ctx context.Context, input graphql2.Desti favFirst := true switch input.FieldID { - case fieldSlackChanID: - res, err := q.SlackChannels(ctx, &graphql2.SlackChannelSearchOptions{ - Omit: input.Omit, - First: input.First, - Search: input.Search, - After: input.After, - }) - if err != nil { - return nil, err - } - - var nodes []graphql2.FieldSearchResult - for _, c := range res.Nodes { - nodes = append(nodes, graphql2.FieldSearchResult{ - FieldID: input.FieldID, - Value: c.ID, - Label: c.Name, - }) - } - - return &graphql2.FieldSearchConnection{ - Nodes: nodes, - PageInfo: res.PageInfo, - }, nil case fieldSlackUGID: res, err := q.SlackUserGroups(ctx, &graphql2.SlackUserGroupSearchOptions{ Omit: input.Omit, @@ -419,26 +387,6 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, }}, }, - { - Type: destSlackChan, - Name: "Slack Channel", - Enabled: cfg.Slack.Enable, - SupportsAlertNotifications: true, - SupportsStatusUpdates: true, - SupportsOnCallNotify: true, - StatusUpdatesRequired: true, - RequiredFields: []nfydest.FieldConfig{{ - FieldID: fieldSlackChanID, - Label: "Slack Channel", - InputType: "text", - SupportsSearch: true, - }}, - DynamicParams: []nfydest.DynamicParamConfig{{ - ParamID: "message", - Label: "Message", - Hint: "The text of the message to send.", - }}, - }, { Type: destSlackUG, Name: "Update Slack User Group", @@ -451,7 +399,7 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] SupportsSearch: true, Hint: "The selected group's membership will be replaced/set to the schedule's on-call user(s).", }, { - FieldID: fieldSlackChanID, + FieldID: slack.FieldSlackChannelID, Label: "Slack Channel (for errors)", InputType: "text", SupportsSearch: true, diff --git a/graphql2/graphqlapp/destinationvalidation.go b/graphql2/graphqlapp/destinationvalidation.go index 2044165713..a3ae010d30 100644 --- a/graphql2/graphqlapp/destinationvalidation.go +++ b/graphql2/graphqlapp/destinationvalidation.go @@ -12,6 +12,7 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/gadb" "github.com/target/goalert/graphql2" + "github.com/target/goalert/notification/slack" "github.com/target/goalert/permission" "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" @@ -152,11 +153,11 @@ func (a *App) ValidateDestination(ctx context.Context, fieldName string, dest *g return addDestFieldError(ctx, fieldName, fieldPhoneNumber, err) } return nil - case destSlackChan: - chanID := dest.Arg(fieldSlackChanID) + case slack.DestTypeSlackChannel: + chanID := dest.Arg(slack.FieldSlackChannelID) err := a.SlackStore.ValidateChannel(ctx, chanID) if err != nil { - return addDestFieldError(ctx, fieldName, fieldSlackChanID, err) + return addDestFieldError(ctx, fieldName, slack.FieldSlackChannelID, err) } return nil @@ -173,10 +174,10 @@ func (a *App) ValidateDestination(ctx context.Context, fieldName string, dest *g return addDestFieldError(ctx, fieldName, fieldSlackUGID, userErr) } - chanID := dest.Arg(fieldSlackChanID) + chanID := dest.Arg(slack.FieldSlackChannelID) chanErr := a.SlackStore.ValidateChannel(ctx, chanID) if chanErr != nil { - return addDestFieldError(ctx, fieldName, fieldSlackChanID, chanErr) + return addDestFieldError(ctx, fieldName, slack.FieldSlackChannelID, chanErr) } return nil From 5c2f4eb8c3b6931167de5df5f6b5d04230ca1922 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 10:48:18 -0500 Subject: [PATCH 08/21] Add DestReg to GraphQL server config - Enhance server configuration with DestReg support for better registry handling. --- app/initgraphql.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/initgraphql.go b/app/initgraphql.go index 861eccd89d..25367c7b4c 100644 --- a/app/initgraphql.go +++ b/app/initgraphql.go @@ -42,6 +42,7 @@ func (app *App) initGraphQL(ctx context.Context) error { AuthLinkStore: app.AuthLinkStore, SWO: app.cfg.SWO, APIKeyStore: app.APIKeyStore, + DestReg: app.DestRegistry, } return nil From 48965efedd29bd4108995906a135b2f9c562f87f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:03:39 -0500 Subject: [PATCH 09/21] add slack DM to registry --- graphql2/graphqlapp/compat.go | 4 +- graphql2/graphqlapp/contactmethod.go | 5 +- graphql2/graphqlapp/destinationdisplayinfo.go | 21 ------ graphql2/graphqlapp/destinationtypes.go | 20 ----- graphql2/graphqlapp/destinationvalidation.go | 6 +- notification/nfydest/registry.go | 1 + notification/slack/nfydestdm.go | 75 +++++++++++++++++++ 7 files changed, 84 insertions(+), 48 deletions(-) create mode 100644 notification/slack/nfydestdm.go diff --git a/graphql2/graphqlapp/compat.go b/graphql2/graphqlapp/compat.go index 4873a25ec8..6f8373294d 100644 --- a/graphql2/graphqlapp/compat.go +++ b/graphql2/graphqlapp/compat.go @@ -94,8 +94,8 @@ func CompatDestToCMTypeVal(d gadb.DestV1) (contactmethod.Type, string) { return contactmethod.TypeEmail, d.Arg(fieldEmailAddress) case destWebhook: return contactmethod.TypeWebhook, d.Arg(fieldWebhookURL) - case destSlackDM: - return contactmethod.TypeSlackDM, d.Arg(fieldSlackUserID) + case slack.DestTypeSlackDirectMessage: + return contactmethod.TypeSlackDM, d.Arg(slack.FieldSlackUserID) } return "", "" diff --git a/graphql2/graphqlapp/contactmethod.go b/graphql2/graphqlapp/contactmethod.go index 1e42881408..c8afcdd0f5 100644 --- a/graphql2/graphqlapp/contactmethod.go +++ b/graphql2/graphqlapp/contactmethod.go @@ -11,6 +11,7 @@ import ( "github.com/target/goalert/gadb" "github.com/target/goalert/graphql2" "github.com/target/goalert/notification" + "github.com/target/goalert/notification/slack" "github.com/target/goalert/notification/webhook" "github.com/target/goalert/user/contactmethod" "github.com/target/goalert/validation" @@ -47,8 +48,8 @@ func (a *ContactMethod) Dest(ctx context.Context, obj *contactmethod.ContactMeth }, nil case contactmethod.TypeSlackDM: return &gadb.DestV1{ - Type: destSlackDM, - Args: map[string]string{fieldSlackUserID: obj.Value}, + Type: slack.DestTypeSlackDirectMessage, + Args: map[string]string{slack.FieldSlackUserID: obj.Value}, }, nil } diff --git a/graphql2/graphqlapp/destinationdisplayinfo.go b/graphql2/graphqlapp/destinationdisplayinfo.go index 068e8734d4..6eaa2262b3 100644 --- a/graphql2/graphqlapp/destinationdisplayinfo.go +++ b/graphql2/graphqlapp/destinationdisplayinfo.go @@ -151,27 +151,6 @@ func (a *Query) _DestinationDisplayInfo(ctx context.Context, dest gadb.DestV1, s IconAltText: "Webhook", Text: u.Hostname(), }, nil - case destSlackDM: - u, err := app.SlackStore.User(ctx, dest.Arg(fieldSlackUserID)) - if err != nil { - return nil, err - } - - team, err := app.SlackStore.Team(ctx, u.TeamID) - if err != nil { - return nil, err - } - - if team.IconURL == "" { - team.IconURL = "builtin://slack" - } - - return &nfydest.DisplayInfo{ - IconURL: team.IconURL, - IconAltText: team.Name, - LinkURL: team.UserLink(u.ID), - Text: u.Name, - }, nil case destSlackUG: ug, err := app.SlackStore.UserGroup(ctx, dest.Arg(fieldSlackUGID)) diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 63dc9dcb69..46ac89a349 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -19,7 +19,6 @@ const ( destTwilioVoice = "builtin-twilio-voice" destSMTP = "builtin-smtp-email" destWebhook = "builtin-webhook" - destSlackDM = "builtin-slack-dm" destSlackUG = "builtin-slack-usergroup" destUser = "builtin-user" destRotation = "builtin-rotation" @@ -29,7 +28,6 @@ const ( fieldPhoneNumber = "phone_number" fieldEmailAddress = "email_address" fieldWebhookURL = "webhook_url" - fieldSlackUserID = "slack_user_id" fieldSlackUGID = "slack_usergroup_id" fieldUserID = "user_id" fieldRotationID = "rotation_id" @@ -369,24 +367,6 @@ func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([] }, }, }, - { - Type: destSlackDM, - Name: "Slack Message (DM)", - Enabled: cfg.Slack.Enable, - SupportsAlertNotifications: true, - SupportsUserVerification: true, - SupportsStatusUpdates: true, - UserVerificationRequired: true, - StatusUpdatesRequired: true, - RequiredFields: []nfydest.FieldConfig{{ - FieldID: fieldSlackUserID, - Label: "Slack User", - PlaceholderText: "member ID", - InputType: "text", - // supportsSearch: true, // TODO: implement search select functionality for users - Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, - }}, - }, { Type: destSlackUG, Name: "Update Slack User Group", diff --git a/graphql2/graphqlapp/destinationvalidation.go b/graphql2/graphqlapp/destinationvalidation.go index a3ae010d30..3c7e203c77 100644 --- a/graphql2/graphqlapp/destinationvalidation.go +++ b/graphql2/graphqlapp/destinationvalidation.go @@ -161,10 +161,10 @@ func (a *App) ValidateDestination(ctx context.Context, fieldName string, dest *g } return nil - case destSlackDM: - userID := dest.Arg(fieldSlackUserID) + case slack.DestTypeSlackDirectMessage: + userID := dest.Arg(slack.FieldSlackUserID) if err := a.SlackStore.ValidateUser(ctx, userID); err != nil { - return addDestFieldError(ctx, fieldName, fieldSlackUserID, err) + return addDestFieldError(ctx, fieldName, slack.FieldSlackUserID, err) } return nil case destSlackUG: diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go index 25359842cb..d2c14b9752 100644 --- a/notification/nfydest/registry.go +++ b/notification/nfydest/registry.go @@ -62,6 +62,7 @@ func (r *Registry) Types(ctx context.Context) ([]TypeInfo, error) { if err != nil { return nil, fmt.Errorf("get type info for %s: %w", id, err) } + ti.Type = id // ensure ID is set out = append(out, *ti) } diff --git a/notification/slack/nfydestdm.go b/notification/slack/nfydestdm.go new file mode 100644 index 0000000000..10bd280ba8 --- /dev/null +++ b/notification/slack/nfydestdm.go @@ -0,0 +1,75 @@ +package slack + +import ( + "context" + + "github.com/target/goalert/config" + "github.com/target/goalert/notification/nfydest" + "github.com/target/goalert/validation" +) + +var _ nfydest.Provider = (*DMSender)(nil) + +func (dm *DMSender) ID() string { return DestTypeSlackDirectMessage } +func (dm *DMSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { + cfg := config.FromContext(ctx) + return &nfydest.TypeInfo{ + Type: DestTypeSlackDirectMessage, + Name: "Slack Message (DM)", + Enabled: cfg.Slack.Enable, + SupportsAlertNotifications: true, + SupportsUserVerification: true, + SupportsStatusUpdates: true, + UserVerificationRequired: true, + StatusUpdatesRequired: true, + RequiredFields: []nfydest.FieldConfig{{ + FieldID: FieldSlackUserID, + Label: "Slack User", + PlaceholderText: "member ID", + InputType: "text", + // supportsSearch: true, // TODO: implement search select functionality for users + Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, + }}, + }, nil +} + +func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) (bool, error) { + switch fieldID { + case FieldSlackChannelID: + err := dm.ValidateUser(ctx, value) + if validation.IsValidationError(err) { + return false, nil + } + + return err == nil, err + } + + return false, validation.NewGenericError("unknown field ID") +} + +func (dm *DMSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { + if args == nil { + args = make(map[string]string) + } + + u, err := dm.User(ctx, args[FieldSlackUserID]) + if err != nil { + return nil, err + } + + team, err := dm.Team(ctx, u.TeamID) + if err != nil { + return nil, err + } + + if team.IconURL == "" { + team.IconURL = "builtin://slack" + } + + return &nfydest.DisplayInfo{ + IconURL: team.IconURL, + IconAltText: team.Name, + LinkURL: team.UserLink(u.ID), + Text: u.Name, + }, nil +} From ffdd2f99affcc08b1064f97af5beef7cf440097b Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:29:12 -0500 Subject: [PATCH 10/21] Refactor validation logic in notification providers - Changed `ValidateField` to return only an error simplifying error handling. - Introduced `ValidateDest` to validate destination and its arguments. - Added `ErrNotEnabled` to handle disabled destination types. - Updated Slack provider implementations to use new validation logic. --- notification/nfydest/provider.go | 2 +- notification/nfydest/registry.go | 5 +- notification/nfydest/validate.go | 86 ++++++++++++++++++++++++++++ notification/slack/nfydestchannel.go | 11 +--- notification/slack/nfydestdm.go | 11 +--- 5 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 notification/nfydest/validate.go diff --git a/notification/nfydest/provider.go b/notification/nfydest/provider.go index 824b561645..9e13108ad7 100644 --- a/notification/nfydest/provider.go +++ b/notification/nfydest/provider.go @@ -6,7 +6,7 @@ type Provider interface { ID() string TypeInfo(ctx context.Context) (*TypeInfo, error) - ValidateField(ctx context.Context, fieldID, value string) (ok bool, err error) + ValidateField(ctx context.Context, fieldID, value string) error DisplayInfo(ctx context.Context, args map[string]string) (*DisplayInfo, error) } diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go index d2c14b9752..7c3879b41c 100644 --- a/notification/nfydest/registry.go +++ b/notification/nfydest/registry.go @@ -12,6 +12,7 @@ import ( var ( ErrUnknownType = validation.NewGenericError("unknown destination type") ErrUnsupported = errors.New("unsupported operation") + ErrNotEnabled = validation.NewGenericError("destination type is not enabled") ) type Registry struct { @@ -46,10 +47,10 @@ func (r *Registry) DisplayInfo(ctx context.Context, d gadb.DestV1) (*DisplayInfo return p.DisplayInfo(ctx, d.Args) } -func (r *Registry) ValidateField(ctx context.Context, typeID, fieldID, value string) (bool, error) { +func (r *Registry) ValidateField(ctx context.Context, typeID, fieldID, value string) error { p := r.Provider(typeID) if p == nil { - return false, ErrUnknownType + return ErrUnknownType } return p.ValidateField(ctx, fieldID, value) diff --git a/notification/nfydest/validate.go b/notification/nfydest/validate.go new file mode 100644 index 0000000000..ef575707f1 --- /dev/null +++ b/notification/nfydest/validate.go @@ -0,0 +1,86 @@ +package nfydest + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/target/goalert/gadb" + "github.com/target/goalert/validation" +) + +type ArgsValidator interface { + ValidateArgs(ctx context.Context, args map[string]string) error +} + +// DestArgError is returned when a destination argument is invalid. +type DestArgError struct { + FieldID string + Err error +} + +func (e *DestArgError) Error() string { return fmt.Sprintf("field %s: %s", e.FieldID, e.Err) } + +func (r *Registry) ValidateDest(ctx context.Context, dest gadb.DestV1) error { + p := r.Provider(dest.Type) + if p == nil { + return ErrUnknownType + } + + info, err := p.TypeInfo(ctx) + if err != nil { + return err + } + + if !info.Enabled { + return ErrNotEnabled + } + + if dest.Args == nil { + dest.Args = make(map[string]string) + } + + if v, ok := p.(ArgsValidator); ok { + // Some providers may need/want to validate all args at once. + err := v.ValidateArgs(ctx, dest.Args) + // If we get `ErrUnsupported`, we'll fall back to the field-by-field validation, this can happen if the provider implements the interface, but the backing implementation (e.g., external plugin) doesn't support it. + if !errors.Is(err, ErrUnsupported) { + return err + } + } + + fieldNames := make([]string, 0, len(info.RequiredFields)) + for _, f := range info.RequiredFields { + fieldNames = append(fieldNames, f.FieldID) + } + + // Make sure we reject any fields that are not expected. + for fName := range dest.Args { + if slices.Contains(fieldNames, fName) { + continue + } + + return &DestArgError{ + FieldID: fName, + Err: fmt.Errorf("unexpected field"), + } + } + + // Make sure all required fields are valid, which may be allowed to be empty (thus we don't iterate over dest.Args). + for _, f := range info.RequiredFields { + err := p.ValidateField(ctx, f.FieldID, dest.Args[f.FieldID]) + if validation.IsClientError(err) { + return &DestArgError{ + FieldID: f.FieldID, + Err: err, + } + } + if err != nil { + return fmt.Errorf("validate field %s: %w", f.FieldID, err) + } + } + + // Since we have no extra/unknown fields, and all required fields are valid, we've validated the destination. + return nil +} diff --git a/notification/slack/nfydestchannel.go b/notification/slack/nfydestchannel.go index e854708efb..3854387603 100644 --- a/notification/slack/nfydestchannel.go +++ b/notification/slack/nfydestchannel.go @@ -39,18 +39,13 @@ func (s *ChannelSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) }, nil } -func (s *ChannelSender) ValidateField(ctx context.Context, fieldID, value string) (bool, error) { +func (s *ChannelSender) ValidateField(ctx context.Context, fieldID, value string) error { switch fieldID { case FieldSlackChannelID: - err := s.ValidateChannel(ctx, value) - if validation.IsValidationError(err) { - return false, nil - } - - return err == nil, err + return s.ValidateChannel(ctx, value) } - return false, validation.NewGenericError("unknown field ID") + return validation.NewGenericError("unknown field ID") } func (s *ChannelSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { diff --git a/notification/slack/nfydestdm.go b/notification/slack/nfydestdm.go index 10bd280ba8..817f006850 100644 --- a/notification/slack/nfydestdm.go +++ b/notification/slack/nfydestdm.go @@ -33,18 +33,13 @@ func (dm *DMSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { }, nil } -func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) (bool, error) { +func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) error { switch fieldID { case FieldSlackChannelID: - err := dm.ValidateUser(ctx, value) - if validation.IsValidationError(err) { - return false, nil - } - - return err == nil, err + return dm.ValidateUser(ctx, value) } - return false, validation.NewGenericError("unknown field ID") + return validation.NewGenericError("unknown field ID") } func (dm *DMSender) DisplayInfo(ctx context.Context, args map[string]string) (*nfydest.DisplayInfo, error) { From f66a078f678691ae2de68d0992092406a571d71e Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:31:54 -0500 Subject: [PATCH 11/21] Enhance destination validation handling - Added validation for notification destinations - Improved error messaging and handling for unsupported destination types - Integrated new error type checks for more accurate error reporting --- graphql2/graphqlapp/destinationvalidation.go | 40 ++++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/graphql2/graphqlapp/destinationvalidation.go b/graphql2/graphqlapp/destinationvalidation.go index 3c7e203c77..07e152fde4 100644 --- a/graphql2/graphqlapp/destinationvalidation.go +++ b/graphql2/graphqlapp/destinationvalidation.go @@ -12,6 +12,7 @@ import ( "github.com/target/goalert/config" "github.com/target/goalert/gadb" "github.com/target/goalert/graphql2" + "github.com/target/goalert/notification/nfydest" "github.com/target/goalert/notification/slack" "github.com/target/goalert/permission" "github.com/target/goalert/validation" @@ -244,19 +245,34 @@ func (a *App) ValidateDestination(ctx context.Context, fieldName string, dest *g return nil } - message := fmt.Sprintf("unsupported destination type: %s", dest.Type) - if dest.Type == "" { - message = "destination type is required" + err = a.DestReg.ValidateDest(ctx, *dest) + if errors.Is(err, nfydest.ErrUnknownType) { + message := fmt.Sprintf("unsupported destination type: %s", dest.Type) + if dest.Type == "" { + message = "destination type is required" + } + + // unsupported destination type + graphql.AddError(ctx, &gqlerror.Error{ + Message: message, + Path: appendPath(ctx, fieldName+".type"), + Extensions: map[string]interface{}{ + "code": graphql2.ErrorCodeInvalidInputValue, + }, + }) + + return errAlreadySet } - // unsupported destination type - graphql.AddError(ctx, &gqlerror.Error{ - Message: message, - Path: appendPath(ctx, fieldName+".type"), - Extensions: map[string]interface{}{ - "code": graphql2.ErrorCodeInvalidInputValue, - }, - }) + var argErr *nfydest.DestArgError + if errors.As(err, &argErr) { + return addDestFieldError(ctx, fieldName, argErr.FieldID, argErr.Err) + } - return errAlreadySet + if err != nil { + // internal error + return err + } + + return nil } From 7b0790a743dc3379ac09b395cd53ab9c1fe55be4 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:32:40 -0500 Subject: [PATCH 12/21] Remove Slack channel and DM destination validations - Removed validation logic for Slack channel and direct message destinations - Simplifies validation process by focusing on supported destination types --- graphql2/graphqlapp/destinationvalidation.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/graphql2/graphqlapp/destinationvalidation.go b/graphql2/graphqlapp/destinationvalidation.go index 07e152fde4..0486409ad8 100644 --- a/graphql2/graphqlapp/destinationvalidation.go +++ b/graphql2/graphqlapp/destinationvalidation.go @@ -154,20 +154,6 @@ func (a *App) ValidateDestination(ctx context.Context, fieldName string, dest *g return addDestFieldError(ctx, fieldName, fieldPhoneNumber, err) } return nil - case slack.DestTypeSlackChannel: - chanID := dest.Arg(slack.FieldSlackChannelID) - err := a.SlackStore.ValidateChannel(ctx, chanID) - if err != nil { - return addDestFieldError(ctx, fieldName, slack.FieldSlackChannelID, err) - } - - return nil - case slack.DestTypeSlackDirectMessage: - userID := dest.Arg(slack.FieldSlackUserID) - if err := a.SlackStore.ValidateUser(ctx, userID); err != nil { - return addDestFieldError(ctx, fieldName, slack.FieldSlackUserID, err) - } - return nil case destSlackUG: ugID := dest.Arg(fieldSlackUGID) userErr := a.SlackStore.ValidateUserGroup(ctx, ugID) From f513fc4b3033343fef55806ba0329757831394bb Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:33:54 -0500 Subject: [PATCH 13/21] Improve error handling in destination field validation - Handle client-side validation errors separately --- graphql2/graphqlapp/destinationtypes.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 46ac89a349..52c1221879 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -242,7 +242,11 @@ func (q *Query) DestinationFieldValidate(ctx context.Context, input graphql2.Des return err == nil, nil } - return q.DestReg.ValidateField(ctx, input.DestType, input.FieldID, input.Value) + err := q.DestReg.ValidateField(ctx, input.DestType, input.FieldID, input.Value) + if validation.IsClientError(err) { + return false, nil + } + return err == nil, err } func (q *Query) DestinationTypes(ctx context.Context, isDynamicAction *bool) ([]nfydest.TypeInfo, error) { From 88163c15d3be2bd49bbf313b3f9c3e026a921221 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 11:38:59 -0500 Subject: [PATCH 14/21] Add support for Slack DM notifications - Registered Slack DM sender in the destination registry - Clarified GraphQL schema with real-time validation support - Fixed validation target for Slack DM to use user ID instead of channel ID --- app/startup.go | 1 + graphql2/graph/destinations.graphqls | 2 +- notification/slack/nfydestdm.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/startup.go b/app/startup.go index 53421b1448..a4140d5c14 100644 --- a/app/startup.go +++ b/app/startup.go @@ -74,6 +74,7 @@ func (app *App) startup(ctx context.Context) error { app.notificationManager.RegisterSender(notification.DestTypeChanWebhook, "webhook-channel", webhook.NewSender(ctx)) app.DestRegistry.RegisterProvider(ctx, app.slackChan) + app.DestRegistry.RegisterProvider(ctx, app.slackChan.DMSender()) app.initStartup(ctx, "Startup.Engine", app.initEngine) app.initStartup(ctx, "Startup.Auth", app.initAuth) diff --git a/graphql2/graph/destinations.graphqls b/graphql2/graph/destinations.graphqls index 9a449545dc..603e7a677b 100644 --- a/graphql2/graph/destinations.graphqls +++ b/graphql2/graph/destinations.graphqls @@ -291,7 +291,7 @@ type DestinationFieldConfig { supportsSearch: Boolean! """ - if true, the destination type supports validation + if true, the destination type supports real-time validation """ supportsValidation: Boolean! } diff --git a/notification/slack/nfydestdm.go b/notification/slack/nfydestdm.go index 817f006850..c87e9202ea 100644 --- a/notification/slack/nfydestdm.go +++ b/notification/slack/nfydestdm.go @@ -35,7 +35,7 @@ func (dm *DMSender) TypeInfo(ctx context.Context) (*nfydest.TypeInfo, error) { func (dm *DMSender) ValidateField(ctx context.Context, fieldID, value string) error { switch fieldID { - case FieldSlackChannelID: + case FieldSlackUserID: return dm.ValidateUser(ctx, value) } From 80174a80b64d1d2bb726f3f94a26bfaaa1bdfbe7 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 12:06:25 -0500 Subject: [PATCH 15/21] Handle special case for Slack User Group in search - Adjust Slack User Group channel search pending migration - Add TODO to remove hack after migration to new system --- graphql2/graphqlapp/destinationtypes.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 63dc9dcb69..5ac2ca9b23 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -195,6 +195,14 @@ func (q *Query) DestinationFieldSearch(ctx context.Context, input graphql2.Desti opts.Search = *input.Search } + if input.DestType == destSlackUG && input.FieldID == slack.FieldSlackChannelID { + // Hack: Slack User Group channel search is a special case, until + // it is migrated to the new search system. + // + // TODO: remove this when slack user group is moved to the nfydest.Registry. + input.DestType = slack.DestTypeSlackChannel + } + res, err := q.DestReg.SearchField(ctx, input.DestType, input.FieldID, opts) if err != nil { return nil, err From 82d696e12e447cf5bc6ced9dfc0c8042af1f5c0d Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:15:52 -0500 Subject: [PATCH 16/21] Handle Slack User Group channel search as special case - Temporarily adjust DestType for Slack channel searches until migration to new system. --- graphql2/graphqlapp/destinationtypes.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/graphql2/graphqlapp/destinationtypes.go b/graphql2/graphqlapp/destinationtypes.go index 5ac2ca9b23..fc02b86650 100644 --- a/graphql2/graphqlapp/destinationtypes.go +++ b/graphql2/graphqlapp/destinationtypes.go @@ -72,6 +72,14 @@ func (q *Query) DestinationFieldValueName(ctx context.Context, input graphql2.De return u.Name, nil } + if input.DestType == destSlackUG && input.FieldID == slack.FieldSlackChannelID { + // Hack: Slack User Group channel search is a special case, until + // it is migrated to the new search system. + // + // TODO: remove this when slack user group is moved to the nfydest.Registry. + input.DestType = slack.DestTypeSlackChannel + } + return q.DestReg.FieldLabel(ctx, input.DestType, input.FieldID, input.Value) } From 29fa4fe839fd327e88f76c5950fcfd4422c36754 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:19:26 -0500 Subject: [PATCH 17/21] fix race --- notification/nfydest/registry.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go index 25359842cb..4bcf59ee93 100644 --- a/notification/nfydest/registry.go +++ b/notification/nfydest/registry.go @@ -20,14 +20,21 @@ type Registry struct { } func NewRegistry() *Registry { - return &Registry{ - providers: make(map[string]Provider), - } + return &Registry{} } -func (r *Registry) Provider(id string) Provider { return r.providers[id] } +func (r *Registry) Provider(id string) Provider { + if r.providers == nil { + return nil + } + + return r.providers[id] +} func (r *Registry) RegisterProvider(ctx context.Context, p Provider) { + if r.providers == nil { + r.providers = make(map[string]Provider) + } if r.Provider(p.ID()) != nil { panic(fmt.Sprintf("provider with ID %s already registered", p.ID())) } From 188b679e6fc15dbfc61fd16ab030f2ad6d70f0d0 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:24:46 -0500 Subject: [PATCH 18/21] Revert "fix race" This reverts commit 29fa4fe839fd327e88f76c5950fcfd4422c36754. --- notification/nfydest/registry.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/notification/nfydest/registry.go b/notification/nfydest/registry.go index 4bcf59ee93..25359842cb 100644 --- a/notification/nfydest/registry.go +++ b/notification/nfydest/registry.go @@ -20,21 +20,14 @@ type Registry struct { } func NewRegistry() *Registry { - return &Registry{} -} - -func (r *Registry) Provider(id string) Provider { - if r.providers == nil { - return nil + return &Registry{ + providers: make(map[string]Provider), } - - return r.providers[id] } +func (r *Registry) Provider(id string) Provider { return r.providers[id] } + func (r *Registry) RegisterProvider(ctx context.Context, p Provider) { - if r.providers == nil { - r.providers = make(map[string]Provider) - } if r.Provider(p.ID()) != nil { panic(fmt.Sprintf("provider with ID %s already registered", p.ID())) } From d322f9f16205a452dfd89b3d110a6b0f6730b87f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:27:57 -0500 Subject: [PATCH 19/21] handle early shutdown --- app/startup.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/startup.go b/app/startup.go index 53421b1448..1a5a3a4d1b 100644 --- a/app/startup.go +++ b/app/startup.go @@ -73,8 +73,6 @@ func (app *App) startup(ctx context.Context) error { app.notificationManager.RegisterSender(notification.DestTypeUserWebhook, "webhook-user", webhook.NewSender(ctx)) app.notificationManager.RegisterSender(notification.DestTypeChanWebhook, "webhook-channel", webhook.NewSender(ctx)) - app.DestRegistry.RegisterProvider(ctx, app.slackChan) - app.initStartup(ctx, "Startup.Engine", app.initEngine) app.initStartup(ctx, "Startup.Auth", app.initAuth) app.initStartup(ctx, "Startup.GraphQL", app.initGraphQL) @@ -88,6 +86,8 @@ func (app *App) startup(ctx context.Context) error { return app.startupErr } + app.DestRegistry.RegisterProvider(ctx, app.slackChan) + err := app.mgr.SetPauseResumer(lifecycle.MultiPauseResume( app.Engine, lifecycle.PauseResumerFunc(app._pause, app._resume), From 2f69da265b297bc111dc68eba2dc645e952a9fdd Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:31:50 -0500 Subject: [PATCH 20/21] Improve error handling in on-call dialogs - Adjust promise handling to check for response errors before calling onClose - Prevent onClose if there is an error to ensure proper error reporting --- .../ScheduleOnCallNotificationsCreateDialog.tsx | 13 ++++++++----- .../ScheduleOnCallNotificationsEditDialog.tsx | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsCreateDialog.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsCreateDialog.tsx index 483254a538..f264c008b9 100644 --- a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsCreateDialog.tsx +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsCreateDialog.tsx @@ -124,11 +124,14 @@ export default function ScheduleOnCallNotificationsCreateDialog( } satisfies SetScheduleOnCallNotificationRulesInput, }, { additionalTypenames: ['Schedule'] }, - ) - .then(onClose) - .catch((err) => { - setErr(err) - }) + ).then((res) => { + if (res.error) { + setErr(res.error) + return + } + + onClose() + }) }} form={form} /> diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsEditDialog.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsEditDialog.tsx index eb64f0d5a2..0d59dfd48d 100644 --- a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsEditDialog.tsx +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsEditDialog.tsx @@ -119,11 +119,14 @@ export default function ScheduleOnCallNotificationsEditDialog( } satisfies SetScheduleOnCallNotificationRulesInput, }, { additionalTypenames: ['Schedule'] }, - ) - .then(onClose) - .catch((err) => { - setErr(err) - }) + ).then((res) => { + if (res.error) { + setErr(res.error) + return + } + + onClose() + }) } form={form} /> From a4261ffeb1ccc3bdaf3e845001aa02b87234e687 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 12 Jul 2024 13:39:32 -0500 Subject: [PATCH 21/21] Set type to channel in case of re-order --- web/src/cypress/e2e/schedules.cy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/cypress/e2e/schedules.cy.ts b/web/src/cypress/e2e/schedules.cy.ts index e6632f8a1d..93ba9f9ce3 100644 --- a/web/src/cypress/e2e/schedules.cy.ts +++ b/web/src/cypress/e2e/schedules.cy.ts @@ -486,6 +486,7 @@ function testSchedules(screen: ScreenFormat): void { cy.dialogTitle('Create Notification Rule') cy.dialogForm({ ruleType: 'on-change', + 'dest.type': 'Slack Channel', slack_channel_id: 'general', }) cy.dialogFinish('Submit') @@ -501,6 +502,7 @@ function testSchedules(screen: ScreenFormat): void { cy.dialogTitle('Create Notification Rule') cy.dialogForm({ ruleType: 'time-of-day', + 'dest.type': 'Slack Channel', time: '00:00', 'weekdayFilter[0]': false, 'weekdayFilter[1]': true,