From 19b48795a72d26e255a25176e8e4146369b992a1 Mon Sep 17 00:00:00 2001 From: Andrew Benington Date: Tue, 17 Oct 2023 11:46:09 -0500 Subject: [PATCH] Squashed commit of the following: commit 6f17c7550109cc95e7e4562b63c6fc9205ffc1b7 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 17 08:44:00 2023 -0700 build(deps-dev): bump @mui/x-data-grid from 6.16.1 to 6.16.2 (#3364) Bumps [@mui/x-data-grid](https://github.com/mui/mui-x/tree/HEAD/packages/grid/x-data-grid) from 6.16.1 to 6.16.2. - [Release notes](https://github.com/mui/mui-x/releases) - [Changelog](https://github.com/mui/mui-x/blob/master/CHANGELOG.md) - [Commits](https://github.com/mui/mui-x/commits/v6.16.2/packages/grid/x-data-grid) --- updated-dependencies: - dependency-name: "@mui/x-data-grid" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 4affe4011dbdd57ff4d95709c38b8a5c3796e237 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 17 08:43:03 2023 -0700 build(deps): bump @babel/traverse from 7.22.20 to 7.23.2 (#3367) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.20 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 12091946b659d0ae553074c24374585b298691d9 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 17 08:41:19 2023 -0700 build(deps-dev): bump @types/react-transition-group from 4.4.6 to 4.4.7 (#3363) Bumps [@types/react-transition-group](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-transition-group) from 4.4.6 to 4.4.7. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-transition-group) --- updated-dependencies: - dependency-name: "@types/react-transition-group" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 53c1c7ec8fa44c2760bf90d61d577dd1a0a7330c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 16 12:22:54 2023 -0700 build(deps): bump google.golang.org/grpc from 1.58.2 to 1.58.3 (#3366) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.2 to 1.58.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.58.2...v1.58.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 48d08f2f86ad0ce93dd693384fbd62882497ed8a Author: Nathaniel Caza Date: Fri Oct 13 11:31:51 2023 -0500 increase variance in delayminutes (#3359) commit 4b95ef8ef093ffd887dd81f24c2021348369ca84 Author: Lakshmi2107 <32451441+Lakshmi2107@users.noreply.github.com> Date: Fri Oct 13 20:45:00 2023 +0530 add partial search for description (#3195) * add partial search for description * add contains func * escape contains search --------- Co-authored-by: Lakshmi V Co-authored-by: Nathaniel Caza commit 367173461a9c38f046b96ed1e4e6788edc21989f Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Oct 12 11:04:33 2023 -0500 build(deps): bump golang.org/x/net from 0.16.0 to 0.17.0 (#3360) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.16.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 190c9e58677066351678f8a4324ec5e42ff854c9 Author: Katie Sydlik-Badgerow <38022260+KatieMSB@users.noreply.github.com> Date: Wed Oct 11 16:34:07 2023 -0400 ui/service-metrics: service metrics hooks (#3300) * add new hook and worker * clean up useServiceMetrics * add comments * use specific type as metric keys * update useServiceMetrics hook and add filtering * update search to include maintenance mode commit 53b2e356b4a936481c9f3d6f4ee6e4fc109e1f04 Author: Nathaniel Caza Date: Wed Oct 11 14:52:30 2023 -0500 use sqlc for user favorites (#3288) commit 2ed8e6f38f7470f342cc91881e6b2f682eb7f1f4 Author: Nathaniel Caza Date: Tue Oct 10 15:47:00 2023 -0500 dev: Update Go 1.21.3 and deps (#3356) * update go to 1.21.3 * update go deps * make generate --------- Co-authored-by: Nathaniel Cook commit 41754c32f19c69630511981bd241d059b93d3eb7 Author: Leya Salazar <96083374+leyasalazar@users.noreply.github.com> Date: Tue Oct 10 14:23:12 2023 -0500 Add Webhook Documentation Links to UI (#3294) * working on formField file * webhook documentation link * delete id from url * removed changes to the forms files * Update web/src/app/escalation-policies/PolicyStepForm.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * remove more innecesary changes from form conversion work * extra line web/src/app/util/NumberField.tsx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/util/MountWatcher.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/users/UserContactMethodForm.tsx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/forms/Form.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/forms/FormContainer.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/forms/FormField.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * extra line web/src/app/forms/context.js Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * remove id * open in new tab * support linking to doc section * link directly to webhook section in new tab * fix comment --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Nathaniel Caza commit 47b35fc1318b9324772145eea469d5b32920c84f Author: Nathaniel Cook Date: Tue Oct 10 12:18:27 2023 -0700 alertmetrics: show metrics up to current time, instead of start of bucket (#3345) * show alerts to now in graph, update tooltip label * show header range properly * update headers * fix title commit 5746a5d48b3b91adc6fbb5fb1870e7be1394b938 Author: Allen Ding <59105963+allending313@users.noreply.github.com> Date: Tue Oct 10 15:11:41 2023 -0400 chore: user contactmethod sqlc (#3310) * convert user/contactmethod to use sqlc * clean up validations * remove unnecessary validation * fix bugs from smoketest * clean up transactions with dbtx * fix transaction logic * remove db from cmstore, update query prefixes * remove unecessary newstore func --------- Co-authored-by: AllenDing Co-authored-by: Nathaniel Caza commit 935a72a50f4822588de6fa5c90a64b6b50e56373 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Oct 10 11:44:11 2023 -0500 Bump @apollo/client from 3.8.4 to 3.8.5 (#3352) Bumps [@apollo/client](https://github.com/apollographql/apollo-client) from 3.8.4 to 3.8.5. - [Release notes](https://github.com/apollographql/apollo-client/releases) - [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-client/compare/v3.8.4...v3.8.5) --- updated-dependencies: - dependency-name: "@apollo/client" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit 4c6a92cf2d9a805eaf99cf1109516c3f8e2cc455 Author: Allen Ding <59105963+allending313@users.noreply.github.com> Date: Tue Oct 10 12:07:45 2023 -0400 feat: add monthly rotations UI (#3257) * add monthly rotation ui features * fix handoff summary and use new shiftLength field * revert unnecessary changes * fix hint alignment * update hint styling to match form helper text * fix ui bugs * fix linting * fix ui bugs * handle rel format in diff. timezone * fix cypress test * Update web/src/app/rotations/RotationForm.tsx Co-authored-by: Nathaniel Caza --------- Co-authored-by: AllenDing Co-authored-by: Nathaniel Caza commit 7e0a73d9e6fce628ff26009308735e4118c827f5 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 9 16:33:29 2023 -0500 Bump golang.org/x/sys from 0.12.0 to 0.13.0 (#3350) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.12.0 to 0.13.0. - [Commits](https://github.com/golang/sys/compare/v0.12.0...v0.13.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nathaniel Caza commit 8d56ab42b4b25210cc8416eac3a265960ff07860 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 9 15:59:37 2023 -0500 Bump @mui/x-data-grid from 6.14.0 to 6.16.1 (#3346) Bumps [@mui/x-data-grid](https://github.com/mui/mui-x/tree/HEAD/packages/grid/x-data-grid) from 6.14.0 to 6.16.1. - [Release notes](https://github.com/mui/mui-x/releases) - [Changelog](https://github.com/mui/mui-x/blob/master/CHANGELOG.md) - [Commits](https://github.com/mui/mui-x/commits/v6.16.1/packages/grid/x-data-grid) --- updated-dependencies: - dependency-name: "@mui/x-data-grid" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nathaniel Caza commit ca9d2bfc1df7ec84c602d4223c07cb55a71773b6 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 9 15:59:27 2023 -0500 Bump github.com/spf13/viper from 1.16.0 to 1.17.0 (#3348) Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.16.0 to 1.17.0. - [Release notes](https://github.com/spf13/viper/releases) - [Commits](https://github.com/spf13/viper/compare/v1.16.0...v1.17.0) --- updated-dependencies: - dependency-name: github.com/spf13/viper dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nathaniel Caza commit 952279bac8e225a11b6b631015c9411b374a33cc Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 9 14:19:29 2023 -0500 Bump @babel/core from 7.22.20 to 7.23.0 (#3354) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.22.20 to 7.23.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.0/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit d2c1a5b424d8090b8995580f6b1394dc4d25b280 Author: Nathaniel Caza Date: Mon Oct 9 13:45:06 2023 -0500 fix wrong param (#3355) commit 15a0cc56b4edc2cd35c089841c32f7583b522a50 Author: Nathaniel Caza Date: Fri Oct 6 09:53:06 2023 -0500 preserve line breaks in markdown (#3344) --- app/initstores.go | 7 +- app/inittwilio.go | 1 + dataloader/loader.go | 10 - devtools/ci/dockerfiles/build-env/Dockerfile | 2 +- devtools/ci/dockerfiles/goalert/Dockerfile | 2 +- devtools/ci/tasks/build-all.yml | 2 +- devtools/ci/tasks/build-binaries.yml | 2 +- devtools/ci/tasks/scripts/codecheck.sh | 2 +- devtools/ci/tasks/test-smoketest.yml | 2 +- devtools/resetdb/datagen.go | 2 +- engine/engine.go | 6 +- escalation/search.go | 2 +- gadb/queries.sql.go | 549 +++++++++++++++++- go.mod | 46 +- go.sum | 88 +-- graphql2/generated.go | 10 +- graphql2/graphqlapp/alert.go | 2 +- graphql2/graphqlapp/contactmethod.go | 6 +- graphql2/graphqlapp/dataloaders.go | 4 +- graphql2/graphqlapp/escalationpolicy.go | 2 +- graphql2/graphqlapp/mutation.go | 6 +- graphql2/graphqlapp/rotation.go | 2 +- graphql2/graphqlapp/schedule.go | 2 +- graphql2/graphqlapp/service.go | 2 +- graphql2/graphqlapp/user.go | 4 +- notification/twilio/carrier.go | 6 +- notification/twilio/config.go | 4 + override/queries.sql | 2 +- package.json | 9 +- schedule/rotation/search.go | 2 +- schedule/search.go | 2 +- search/render.go | 8 + service/search.go | 2 +- sqlc.yaml | 2 + user/contactmethod/queries.sql | 132 +++++ user/contactmethod/store.go | 285 +++------ user/favorite/queries.sql | 37 ++ user/favorite/store.go | 182 ++---- .../useServiceMetrics.ts | 77 +++ .../admin-service-metrics/useServices.ts | 136 +++++ web/src/app/documentation/Documentation.tsx | 29 +- .../app/escalation-policies/PolicyStepForm.js | 6 + web/src/app/forms/FormField.js | 2 +- web/src/app/rotations/HandoffSummary.tsx | 17 +- web/src/app/rotations/RotationForm.tsx | 61 +- .../services/AlertMetrics/AlertCountGraph.tsx | 6 +- .../services/AlertMetrics/AlertMetrics.tsx | 18 +- .../AlertMetrics/AlertMetricsFilter.tsx | 2 +- .../services/AlertMetrics/useAlertMetrics.ts | 17 +- web/src/app/users/UserContactMethodForm.tsx | 6 + web/src/app/util/Markdown.js | 3 +- web/src/app/util/timeFormat.test.ts | 13 + web/src/app/util/timeFormat.ts | 7 +- web/src/app/worker/methods.ts | 2 + web/src/cypress/e2e/rotations.cy.ts | 30 +- yarn.lock | 311 +++++++++- 56 files changed, 1656 insertions(+), 523 deletions(-) create mode 100644 user/contactmethod/queries.sql create mode 100644 user/favorite/queries.sql create mode 100644 web/src/app/admin/admin-service-metrics/useServiceMetrics.ts create mode 100644 web/src/app/admin/admin-service-metrics/useServices.ts diff --git a/app/initstores.go b/app/initstores.go index 18c2b55261..79984f8529 100644 --- a/app/initstores.go +++ b/app/initstores.go @@ -153,10 +153,7 @@ func (app *App) initStores(ctx context.Context) error { } if app.ContactMethodStore == nil { - app.ContactMethodStore, err = contactmethod.NewStore(ctx, app.db) - } - if err != nil { - return errors.Wrap(err, "init contact method store") + app.ContactMethodStore = &contactmethod.Store{} } if app.NotificationRuleStore == nil { @@ -244,7 +241,7 @@ func (app *App) initStores(ctx context.Context) error { } if app.FavoriteStore == nil { - app.FavoriteStore, err = favorite.NewStore(ctx, app.db) + app.FavoriteStore, err = favorite.NewStore(ctx) } if err != nil { return errors.Wrap(err, "init favorite store") diff --git a/app/inittwilio.go b/app/inittwilio.go index 2b28958244..99f65fe324 100644 --- a/app/inittwilio.go +++ b/app/inittwilio.go @@ -13,6 +13,7 @@ func (app *App) initTwilio(ctx context.Context) error { app.twilioConfig = &twilio.Config{ BaseURL: app.cfg.TwilioBaseURL, CMStore: app.ContactMethodStore, + DB: app.db, } var err error diff --git a/dataloader/loader.go b/dataloader/loader.go index 66b0b8776b..845c77057a 100644 --- a/dataloader/loader.go +++ b/dataloader/loader.go @@ -37,16 +37,6 @@ func NewStoreLoaderWithDB[V any]( }) } -func NewStoreLoaderIntWithDB[V any]( - ctx context.Context, - db gadb.DBTX, - fetchMany func(context.Context, gadb.DBTX, []int) ([]V, error), -) *Loader[int, V] { - return NewStoreLoaderInt(ctx, func(ctx context.Context, ids []int) ([]V, error) { - return fetchMany(ctx, db, ids) - }) -} - type loaderConfig[K comparable, V any] struct { FetchFunc func(context.Context, []K) ([]V, error) // FetchFunc should return resources for the provided IDs (order doesn't matter). IDFunc func(V) K // Should return the unique ID for a given resource. diff --git a/devtools/ci/dockerfiles/build-env/Dockerfile b/devtools/ci/dockerfiles/build-env/Dockerfile index 75c7201526..0be7c5a366 100644 --- a/devtools/ci/dockerfiles/build-env/Dockerfile +++ b/devtools/ci/dockerfiles/build-env/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/golang:1.21.1-bullseye +FROM docker.io/library/golang:1.21.3-bullseye RUN apt-get update && apt-get install -y \ apt-transport-https \ diff --git a/devtools/ci/dockerfiles/goalert/Dockerfile b/devtools/ci/dockerfiles/goalert/Dockerfile index 7b8a47bc02..1a62a4549a 100644 --- a/devtools/ci/dockerfiles/goalert/Dockerfile +++ b/devtools/ci/dockerfiles/goalert/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/goalert/build-env:go1.21.1-postgres13 AS build +FROM docker.io/goalert/build-env:go1.21.3-postgres13 AS build COPY / /build/ WORKDIR /build RUN make clean bin/build/goalert-linux-amd64 diff --git a/devtools/ci/tasks/build-all.yml b/devtools/ci/tasks/build-all.yml index 520ec29035..7954c3cd06 100644 --- a/devtools/ci/tasks/build-all.yml +++ b/devtools/ci/tasks/build-all.yml @@ -10,7 +10,7 @@ outputs: - name: bin image_resource: type: registry-image - source: { repository: goalert/build-env, tag: go1.21.1-postgres13 } + source: { repository: goalert/build-env, tag: go1.21.3-postgres13 } run: path: sh dir: goalert diff --git a/devtools/ci/tasks/build-binaries.yml b/devtools/ci/tasks/build-binaries.yml index d611cc2aa0..a1cc10538e 100644 --- a/devtools/ci/tasks/build-binaries.yml +++ b/devtools/ci/tasks/build-binaries.yml @@ -13,7 +13,7 @@ outputs: - name: bin image_resource: type: registry-image - source: { repository: goalert/build-env, tag: go1.21.1-postgres13 } + source: { repository: goalert/build-env, tag: go1.21.3-postgres13 } run: path: sh dir: goalert diff --git a/devtools/ci/tasks/scripts/codecheck.sh b/devtools/ci/tasks/scripts/codecheck.sh index e5773e9f3b..4f5339c90d 100755 --- a/devtools/ci/tasks/scripts/codecheck.sh +++ b/devtools/ci/tasks/scripts/codecheck.sh @@ -12,7 +12,7 @@ if [ "$PKG_JSON_VER" != "$DOCKERFILE_VER" ]; then fi # assert build-env versions are identical -BUILD_ENV_VER=go1.21.1-postgres13 +BUILD_ENV_VER=go1.21.3-postgres13 for file in $(find devtools -name 'Dockerfile*'); do if ! grep -q "goalert/build-env" "$file"; then continue diff --git a/devtools/ci/tasks/test-smoketest.yml b/devtools/ci/tasks/test-smoketest.yml index c5aab76f5c..f6d6a14fdc 100644 --- a/devtools/ci/tasks/test-smoketest.yml +++ b/devtools/ci/tasks/test-smoketest.yml @@ -8,7 +8,7 @@ outputs: - name: debug image_resource: type: registry-image - source: { repository: goalert/build-env, tag: go1.21.1-postgres13 } + source: { repository: goalert/build-env, tag: go1.21.3-postgres13 } run: path: sh dir: goalert diff --git a/devtools/resetdb/datagen.go b/devtools/resetdb/datagen.go index bd6bfb7440..6f5f1091b1 100644 --- a/devtools/resetdb/datagen.go +++ b/devtools/resetdb/datagen.go @@ -168,7 +168,7 @@ func (d *datagen) NewNR(userID, cmID string) { ID: d.UUID(), UserID: userID, ContactMethodID: cmID, - DelayMinutes: d.ints.Gen(60, cmID), + DelayMinutes: d.ints.Gen(600, cmID), } d.NotificationRules = append(d.NotificationRules, nr) } diff --git a/engine/engine.go b/engine/engine.go index 0999d58669..18284e7c49 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -358,7 +358,7 @@ func (p *Engine) Receive(ctx context.Context, callbackID string, result notifica var usr *user.User permission.SudoContext(ctx, func(ctx context.Context) { - cm, serr := p.cfg.ContactMethodStore.FindOne(ctx, cb.ContactMethodID) + cm, serr := p.cfg.ContactMethodStore.FindOne(ctx, p.b.db, cb.ContactMethodID) if serr != nil { err = errors.Wrap(serr, "lookup contact method") return @@ -411,7 +411,7 @@ func (p *Engine) Start(ctx context.Context, d notification.Dest) error { var err error permission.SudoContext(ctx, func(ctx context.Context) { - err = p.cfg.ContactMethodStore.EnableByValue(ctx, d.Type.CMType(), d.Value) + err = p.cfg.ContactMethodStore.EnableByValue(ctx, p.b.db, d.Type.CMType(), d.Value) }) return err @@ -426,7 +426,7 @@ func (p *Engine) Stop(ctx context.Context, d notification.Dest) error { var err error permission.SudoContext(ctx, func(ctx context.Context) { - err = p.cfg.ContactMethodStore.DisableByValue(ctx, d.Type.CMType(), d.Value) + err = p.cfg.ContactMethodStore.DisableByValue(ctx, p.b.db, d.Type.CMType(), d.Value) }) return err diff --git a/escalation/search.go b/escalation/search.go index 4f376afeed..4ee720fe94 100644 --- a/escalation/search.go +++ b/escalation/search.go @@ -55,7 +55,7 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers() AND NOT pol.id = any(:omit) {{end}} {{if .Search}} - AND {{orderedPrefixSearch "search" "pol.name"}} + AND ({{orderedPrefixSearch "search" "pol.name"}} OR {{contains "search" "pol.description"}}) {{end}} {{if .After.Name}} AND diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 98b65db3b6..b61f67f7ba 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -714,6 +714,408 @@ func (q *Queries) CalSubUserNames(ctx context.Context, dollar_1 []uuid.UUID) ([] return items, nil } +const contactMethodAdd = `-- name: ContactMethodAdd :exec +INSERT INTO user_contact_methods(id, name, type, value, disabled, user_id, enable_status_updates) + VALUES ($1, $2, $3, $4, $5, $6, $7) +` + +type ContactMethodAddParams struct { + ID uuid.UUID + Name string + Type EnumUserContactMethodType + Value string + Disabled bool + UserID uuid.UUID + EnableStatusUpdates bool +} + +func (q *Queries) ContactMethodAdd(ctx context.Context, arg ContactMethodAddParams) error { + _, err := q.db.ExecContext(ctx, contactMethodAdd, + arg.ID, + arg.Name, + arg.Type, + arg.Value, + arg.Disabled, + arg.UserID, + arg.EnableStatusUpdates, + ) + return err +} + +const contactMethodDisable = `-- name: ContactMethodDisable :one +UPDATE + user_contact_methods +SET + disabled = TRUE +WHERE + type = $1 + AND value = $2 +RETURNING + id +` + +type ContactMethodDisableParams struct { + Type EnumUserContactMethodType + Value string +} + +func (q *Queries) ContactMethodDisable(ctx context.Context, arg ContactMethodDisableParams) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, contactMethodDisable, arg.Type, arg.Value) + var id uuid.UUID + err := row.Scan(&id) + return id, err +} + +const contactMethodEnable = `-- name: ContactMethodEnable :one +UPDATE + user_contact_methods +SET + disabled = FALSE +WHERE + type = $1 + AND value = $2 +RETURNING + id +` + +type ContactMethodEnableParams struct { + Type EnumUserContactMethodType + Value string +} + +func (q *Queries) ContactMethodEnable(ctx context.Context, arg ContactMethodEnableParams) (uuid.UUID, error) { + row := q.db.QueryRowContext(ctx, contactMethodEnable, arg.Type, arg.Value) + var id uuid.UUID + err := row.Scan(&id) + return id, err +} + +const contactMethodFindAll = `-- name: ContactMethodFindAll :many +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + user_id = $1 +` + +type ContactMethodFindAllRow struct { + ID uuid.UUID + Name string + Type EnumUserContactMethodType + Value string + Disabled bool + UserID uuid.UUID + LastTestVerifyAt sql.NullTime + EnableStatusUpdates bool + Pending bool +} + +func (q *Queries) ContactMethodFindAll(ctx context.Context, userID uuid.UUID) ([]ContactMethodFindAllRow, error) { + rows, err := q.db.QueryContext(ctx, contactMethodFindAll, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ContactMethodFindAllRow + for rows.Next() { + var i ContactMethodFindAllRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Type, + &i.Value, + &i.Disabled, + &i.UserID, + &i.LastTestVerifyAt, + &i.EnableStatusUpdates, + &i.Pending, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const contactMethodFindMany = `-- name: ContactMethodFindMany :many +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = ANY ($1::uuid[]) +` + +type ContactMethodFindManyRow struct { + ID uuid.UUID + Name string + Type EnumUserContactMethodType + Value string + Disabled bool + UserID uuid.UUID + LastTestVerifyAt sql.NullTime + EnableStatusUpdates bool + Pending bool +} + +func (q *Queries) ContactMethodFindMany(ctx context.Context, dollar_1 []uuid.UUID) ([]ContactMethodFindManyRow, error) { + rows, err := q.db.QueryContext(ctx, contactMethodFindMany, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ContactMethodFindManyRow + for rows.Next() { + var i ContactMethodFindManyRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Type, + &i.Value, + &i.Disabled, + &i.UserID, + &i.LastTestVerifyAt, + &i.EnableStatusUpdates, + &i.Pending, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const contactMethodFindOneUpdate = `-- name: ContactMethodFindOneUpdate :one +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = $1 +FOR UPDATE +` + +type ContactMethodFindOneUpdateRow struct { + ID uuid.UUID + Name string + Type EnumUserContactMethodType + Value string + Disabled bool + UserID uuid.UUID + LastTestVerifyAt sql.NullTime + EnableStatusUpdates bool + Pending bool +} + +func (q *Queries) ContactMethodFindOneUpdate(ctx context.Context, id uuid.UUID) (ContactMethodFindOneUpdateRow, error) { + row := q.db.QueryRowContext(ctx, contactMethodFindOneUpdate, id) + var i ContactMethodFindOneUpdateRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Type, + &i.Value, + &i.Disabled, + &i.UserID, + &i.LastTestVerifyAt, + &i.EnableStatusUpdates, + &i.Pending, + ) + return i, err +} + +const contactMethodFineOne = `-- name: ContactMethodFineOne :one +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = $1 +` + +type ContactMethodFineOneRow struct { + ID uuid.UUID + Name string + Type EnumUserContactMethodType + Value string + Disabled bool + UserID uuid.UUID + LastTestVerifyAt sql.NullTime + EnableStatusUpdates bool + Pending bool +} + +func (q *Queries) ContactMethodFineOne(ctx context.Context, id uuid.UUID) (ContactMethodFineOneRow, error) { + row := q.db.QueryRowContext(ctx, contactMethodFineOne, id) + var i ContactMethodFineOneRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Type, + &i.Value, + &i.Disabled, + &i.UserID, + &i.LastTestVerifyAt, + &i.EnableStatusUpdates, + &i.Pending, + ) + return i, err +} + +const contactMethodLookupUserID = `-- name: ContactMethodLookupUserID :many +SELECT DISTINCT + user_id +FROM + user_contact_methods +WHERE + id = ANY ($1::uuid[]) +` + +func (q *Queries) ContactMethodLookupUserID(ctx context.Context, dollar_1 []uuid.UUID) ([]uuid.UUID, error) { + rows, err := q.db.QueryContext(ctx, contactMethodLookupUserID, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []uuid.UUID + for rows.Next() { + var user_id uuid.UUID + if err := rows.Scan(&user_id); err != nil { + return nil, err + } + items = append(items, user_id) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const contactMethodMetaTV = `-- name: ContactMethodMetaTV :one +SELECT + coalesce(metadata, '{}'), + now()::timestamptz AS now +FROM + user_contact_methods +WHERE + type = $1 + AND value = $2 +` + +type ContactMethodMetaTVParams struct { + Type EnumUserContactMethodType + Value string +} + +type ContactMethodMetaTVRow struct { + Metadata json.RawMessage + Now time.Time +} + +func (q *Queries) ContactMethodMetaTV(ctx context.Context, arg ContactMethodMetaTVParams) (ContactMethodMetaTVRow, error) { + row := q.db.QueryRowContext(ctx, contactMethodMetaTV, arg.Type, arg.Value) + var i ContactMethodMetaTVRow + err := row.Scan(&i.Metadata, &i.Now) + return i, err +} + +const contactMethodUpdate = `-- name: ContactMethodUpdate :exec +UPDATE + user_contact_methods +SET + name = $2, + disabled = $3, + enable_status_updates = $4 +WHERE + id = $1 +` + +type ContactMethodUpdateParams struct { + ID uuid.UUID + Name string + Disabled bool + EnableStatusUpdates bool +} + +func (q *Queries) ContactMethodUpdate(ctx context.Context, arg ContactMethodUpdateParams) error { + _, err := q.db.ExecContext(ctx, contactMethodUpdate, + arg.ID, + arg.Name, + arg.Disabled, + arg.EnableStatusUpdates, + ) + return err +} + +const contactMethodUpdateMetaTV = `-- name: ContactMethodUpdateMetaTV :exec +UPDATE + user_contact_methods +SET + metadata = jsonb_set(jsonb_set(metadata, '{CarrierV1}', $3::jsonb), '{CarrierV1,UpdatedAt}',('"' || NOW()::timestamptz AT TIME ZONE 'UTC' || '"')::jsonb) +WHERE + type = $1 + AND value = $2 +` + +type ContactMethodUpdateMetaTVParams struct { + Type EnumUserContactMethodType + Value string + CarrierV1 json.RawMessage +} + +func (q *Queries) ContactMethodUpdateMetaTV(ctx context.Context, arg ContactMethodUpdateMetaTVParams) error { + _, err := q.db.ExecContext(ctx, contactMethodUpdateMetaTV, arg.Type, arg.Value, arg.CarrierV1) + return err +} + const createCalSub = `-- name: CreateCalSub :one INSERT INTO user_calendar_subscriptions(id, NAME, user_id, disabled, schedule_id, config) VALUES ($1, $2, $3, $4, $5, $6) @@ -744,6 +1146,16 @@ func (q *Queries) CreateCalSub(ctx context.Context, arg CreateCalSubParams) (tim return created_at, err } +const deleteContactMethod = `-- name: DeleteContactMethod :exec +DELETE FROM user_contact_methods +WHERE id = ANY ($1::uuid[]) +` + +func (q *Queries) DeleteContactMethod(ctx context.Context, dollar_1 []uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteContactMethod, pq.Array(dollar_1)) + return err +} + const deleteManyCalSub = `-- name: DeleteManyCalSub :exec DELETE FROM user_calendar_subscriptions WHERE id = ANY ($1::uuid[]) @@ -1181,7 +1593,7 @@ AND ( AND ( /* only include overrides that start before/within the search end */ $7::timestamptz ISNULL - OR o.start_time <= $6) + OR o.start_time <= $7) AND ( /* resume search after specified "cursor" override */ $8::uuid ISNULL @@ -2078,3 +2490,138 @@ func (q *Queries) UpdateCalSub(ctx context.Context, arg UpdateCalSubParams) erro ) return err } + +const userFavFindAll = `-- name: UserFavFindAll :many +SELECT + tgt_service_id, + tgt_schedule_id, + tgt_rotation_id, + tgt_escalation_policy_id, + tgt_user_id +FROM + user_favorites +WHERE + user_id = $1 + AND ((tgt_service_id NOTNULL + AND $2::bool) + OR (tgt_schedule_id NOTNULL + AND $3::bool) + OR (tgt_rotation_id NOTNULL + AND $4::bool) + OR (tgt_escalation_policy_id NOTNULL + AND $5::bool) + OR (tgt_user_id NOTNULL + AND $6::bool)) +` + +type UserFavFindAllParams struct { + UserID uuid.UUID + AllowServices bool + AllowSchedules bool + AllowRotations bool + AllowEscalationPolicies bool + AllowUsers bool +} + +type UserFavFindAllRow struct { + TgtServiceID uuid.NullUUID + TgtScheduleID uuid.NullUUID + TgtRotationID uuid.NullUUID + TgtEscalationPolicyID uuid.NullUUID + TgtUserID uuid.NullUUID +} + +func (q *Queries) UserFavFindAll(ctx context.Context, arg UserFavFindAllParams) ([]UserFavFindAllRow, error) { + rows, err := q.db.QueryContext(ctx, userFavFindAll, + arg.UserID, + arg.AllowServices, + arg.AllowSchedules, + arg.AllowRotations, + arg.AllowEscalationPolicies, + arg.AllowUsers, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UserFavFindAllRow + for rows.Next() { + var i UserFavFindAllRow + if err := rows.Scan( + &i.TgtServiceID, + &i.TgtScheduleID, + &i.TgtRotationID, + &i.TgtEscalationPolicyID, + &i.TgtUserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const userFavSet = `-- name: UserFavSet :exec +INSERT INTO user_favorites(user_id, tgt_service_id, tgt_schedule_id, tgt_rotation_id, tgt_escalation_policy_id, tgt_user_id) + VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT + DO NOTHING +` + +type UserFavSetParams struct { + UserID uuid.UUID + TgtServiceID uuid.NullUUID + TgtScheduleID uuid.NullUUID + TgtRotationID uuid.NullUUID + TgtEscalationPolicyID uuid.NullUUID + TgtUserID uuid.NullUUID +} + +func (q *Queries) UserFavSet(ctx context.Context, arg UserFavSetParams) error { + _, err := q.db.ExecContext(ctx, userFavSet, + arg.UserID, + arg.TgtServiceID, + arg.TgtScheduleID, + arg.TgtRotationID, + arg.TgtEscalationPolicyID, + arg.TgtUserID, + ) + return err +} + +const userFavUnset = `-- name: UserFavUnset :exec +DELETE FROM user_favorites +WHERE user_id = $1 + AND tgt_service_id = $2 + OR tgt_schedule_id = $3 + OR tgt_rotation_id = $4 + OR tgt_escalation_policy_id = $5 + OR tgt_user_id = $6 +` + +type UserFavUnsetParams struct { + UserID uuid.UUID + TgtServiceID uuid.NullUUID + TgtScheduleID uuid.NullUUID + TgtRotationID uuid.NullUUID + TgtEscalationPolicyID uuid.NullUUID + TgtUserID uuid.NullUUID +} + +func (q *Queries) UserFavUnset(ctx context.Context, arg UserFavUnsetParams) error { + _, err := q.db.ExecContext(ctx, userFavUnset, + arg.UserID, + arg.TgtServiceID, + arg.TgtScheduleID, + arg.TgtRotationID, + arg.TgtEscalationPolicyID, + arg.TgtUserID, + ) + return err +} diff --git a/go.mod b/go.mod index ac457b968e..ef826fc9b8 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/target/goalert go 1.21 require ( - github.com/99designs/gqlgen v0.17.38 + github.com/99designs/gqlgen v0.17.39 github.com/brianvoe/gofakeit/v6 v6.23.2 github.com/coreos/go-oidc/v3 v3.6.0 github.com/creack/pty v1.1.18 - github.com/davecgh/go-spew v1.1.1 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/emersion/go-smtp v0.18.1 github.com/fatih/color v1.15.0 github.com/felixge/httpsnoop v1.0.3 @@ -39,16 +39,16 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/slack-go/slack v0.12.3 github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.16.0 + github.com/spf13/viper v1.17.0 github.com/sqlc-dev/pqtype v0.3.0 github.com/stretchr/testify v1.8.4 github.com/vektah/gqlparser/v2 v2.5.10 - golang.org/x/crypto v0.13.0 - golang.org/x/oauth2 v0.12.0 - golang.org/x/sys v0.12.0 - golang.org/x/term v0.12.0 - golang.org/x/tools v0.13.0 - google.golang.org/grpc v1.58.2 + golang.org/x/crypto v0.14.0 + golang.org/x/oauth2 v0.13.0 + golang.org/x/sys v0.13.0 + golang.org/x/term v0.13.0 + golang.org/x/tools v0.14.0 + google.golang.org/grpc v1.58.3 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 google.golang.org/protobuf v1.31.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -77,7 +77,7 @@ require ( github.com/cloudflare/circl v1.3.3 // indirect github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect github.com/envoyproxy/go-control-plane v0.11.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect @@ -126,34 +126,40 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/sosodev/duration v1.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/t-k/fluent-logger-golang v1.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/urfave/cli/v2 v2.25.7 // indirect github.com/vanng822/css v1.0.1 // indirect github.com/vanng822/go-premailer v1.20.2 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sync v0.3.0 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 8c65f22f41..c0837d2d43 100644 --- a/go.sum +++ b/go.sum @@ -598,8 +598,8 @@ cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcP dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/99designs/gqlgen v0.17.38 h1:3r7G7i8UAdY0iYreNiBAA55auVsrowO0+ZhMl5g4GYU= -github.com/99designs/gqlgen v0.17.38/go.mod h1:2v+dKtpI8mIzYeW9dYN8mO69tMmjszW2xKLNcWR/5wQ= +github.com/99designs/gqlgen v0.17.39 h1:wPTAyc2fqVjAWT5DsJ21k/lLudgnXzURwbsjVNegFpU= +github.com/99designs/gqlgen v0.17.39/go.mod h1:b62q1USk82GYIVjC60h02YguAZLqYZtvWml8KkhJps4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -693,15 +693,17 @@ github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -1105,8 +1107,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1138,6 +1141,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -1154,22 +1161,24 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4= +github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= @@ -1192,8 +1201,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/t-k/fluent-logger-golang v1.0.0 h1:4IQzY+/l66Zkkhk9eB3LwF9vPkgKHJ1rpYdrRiap0EI= github.com/t-k/fluent-logger-golang v1.0.0/go.mod h1:6vC3Vzp9Kva0l5J9+YDY5/ROePwkAqwLK+KneCjSm4w= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= @@ -1237,9 +1246,13 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -1264,8 +1277,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1281,6 +1294,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df h1:jfUqBujZx2dktJVEmZpCkyngz7MWrVv1y9kLOqFNsqw= golang.org/x/exp/typeparams v0.0.0-20230626212559-97b1e661b5df/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= @@ -1325,8 +1340,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1391,8 +1406,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1422,8 +1437,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1440,8 +1455,9 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1530,8 +1546,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1541,8 +1557,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1635,8 +1651,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1854,16 +1870,16 @@ google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1905,8 +1921,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= diff --git a/graphql2/generated.go b/graphql2/generated.go index 47232822cc..5f8ed87b58 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -50,6 +50,7 @@ import ( // NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { return &executableSchema{ + schema: cfg.Schema, resolvers: cfg.Resolvers, directives: cfg.Directives, complexity: cfg.Complexity, @@ -57,6 +58,7 @@ func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { } type Config struct { + Schema *ast.Schema Resolvers ResolverRoot Directives DirectiveRoot Complexity ComplexityRoot @@ -981,12 +983,16 @@ type UserOverrideResolver interface { } type executableSchema struct { + schema *ast.Schema resolvers ResolverRoot directives DirectiveRoot complexity ComplexityRoot } func (e *executableSchema) Schema() *ast.Schema { + if e.schema != nil { + return e.schema + } return parsedSchema } @@ -4571,14 +4577,14 @@ func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { if ec.DisableIntrospection { return nil, errors.New("introspection disabled") } - return introspection.WrapSchema(parsedSchema), nil + return introspection.WrapSchema(ec.Schema()), nil } func (ec *executionContext) introspectType(name string) (*introspection.Type, error) { if ec.DisableIntrospection { return nil, errors.New("introspection disabled") } - return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil + return introspection.WrapTypeFromDef(ec.Schema(), ec.Schema().Types[name]), nil } //go:embed "schema.graphql" diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index 08be6e4ae4..cbaea469b5 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -188,7 +188,7 @@ func (q *Query) Alert(ctx context.Context, alertID int) (*alert.Alert, error) { * Merges favorites and user-specified serviceIDs in opts.FilterByServiceID */ func (q *Query) mergeFavorites(ctx context.Context, svcs []string) ([]string, error) { - targets, err := q.FavoriteStore.FindAll(ctx, permission.UserID(ctx), []assignment.TargetType{assignment.TargetTypeService}) + targets, err := q.FavoriteStore.FindAll(ctx, q.DB, permission.UserID(ctx), []assignment.TargetType{assignment.TargetTypeService}) if err != nil { return nil, err } diff --git a/graphql2/graphqlapp/contactmethod.go b/graphql2/graphqlapp/contactmethod.go index c77277a6f1..43d5c5d585 100644 --- a/graphql2/graphqlapp/contactmethod.go +++ b/graphql2/graphqlapp/contactmethod.go @@ -112,7 +112,7 @@ func (m *Mutation) CreateUserContactMethod(ctx context.Context, input graphql2.C err := withContextTx(ctx, m.DB, func(ctx context.Context, tx *sql.Tx) error { var err error - cm, err = m.CMStore.CreateTx(ctx, tx, &contactmethod.ContactMethod{ + cm, err = m.CMStore.Create(ctx, tx, &contactmethod.ContactMethod{ Name: input.Name, Type: input.Type, UserID: input.UserID, @@ -144,7 +144,7 @@ func (m *Mutation) CreateUserContactMethod(ctx context.Context, input graphql2.C func (m *Mutation) UpdateUserContactMethod(ctx context.Context, input graphql2.UpdateUserContactMethodInput) (bool, error) { err := withContextTx(ctx, m.DB, func(ctx context.Context, tx *sql.Tx) error { - cm, err := m.CMStore.FindOneTx(ctx, tx, input.ID) + cm, err := m.CMStore.FindOne(ctx, tx, input.ID) if errors.Is(err, sql.ErrNoRows) { return validation.NewFieldError("id", "contact method not found") } @@ -161,7 +161,7 @@ func (m *Mutation) UpdateUserContactMethod(ctx context.Context, input graphql2.U cm.StatusUpdates = *input.EnableStatusUpdates } - return m.CMStore.UpdateTx(ctx, tx, cm) + return m.CMStore.Update(ctx, tx, cm) }) return err == nil, err } diff --git a/graphql2/graphqlapp/dataloaders.go b/graphql2/graphqlapp/dataloaders.go index b9879e7e1f..e906064504 100644 --- a/graphql2/graphqlapp/dataloaders.go +++ b/graphql2/graphqlapp/dataloaders.go @@ -55,7 +55,7 @@ func (a *App) registerLoaders(ctx context.Context) context.Context { ctx = context.WithValue(ctx, dataLoaderKeySchedule, dataloader.NewStoreLoader(ctx, a.ScheduleStore.FindMany)) ctx = context.WithValue(ctx, dataLoaderKeyService, dataloader.NewStoreLoader(ctx, a.ServiceStore.FindMany)) ctx = context.WithValue(ctx, dataLoaderKeyUser, dataloader.NewStoreLoader(ctx, a.UserStore.FindMany)) - ctx = context.WithValue(ctx, dataLoaderKeyCM, dataloader.NewStoreLoader(ctx, a.CMStore.FindMany)) + ctx = context.WithValue(ctx, dataLoaderKeyCM, dataloader.NewStoreLoaderWithDB(ctx, a.DB, a.CMStore.FindMany)) ctx = context.WithValue(ctx, dataLoaderKeyNotificationMessageStatus, dataloader.NewStoreLoader(ctx, a.NotificationStore.FindManyMessageStatuses)) ctx = context.WithValue(ctx, dataLoaderKeyHeartbeatMonitor, dataloader.NewStoreLoader(ctx, a.HeartbeatStore.FindMany)) ctx = context.WithValue(ctx, dataLoaderKeyNC, dataloader.NewStoreLoader(ctx, a.NCStore.FindMany)) @@ -152,7 +152,7 @@ func (app *App) FindOneAlertMetric(ctx context.Context, id int) (*alertmetrics.M func (app *App) FindOneCM(ctx context.Context, id string) (*contactmethod.ContactMethod, error) { loader, ok := ctx.Value(dataLoaderKeyCM).(*dataloader.Loader[string, contactmethod.ContactMethod]) if !ok { - return app.CMStore.FindOne(ctx, id) + return app.CMStore.FindOne(ctx, app.DB, id) } return loader.FetchOne(ctx, id) diff --git a/graphql2/graphqlapp/escalationpolicy.go b/graphql2/graphqlapp/escalationpolicy.go index 10221cf9da..228fc1df41 100644 --- a/graphql2/graphqlapp/escalationpolicy.go +++ b/graphql2/graphqlapp/escalationpolicy.go @@ -141,7 +141,7 @@ func (m *Mutation) CreateEscalationPolicy(ctx context.Context, input graphql2.Cr return err } if input.Favorite != nil && *input.Favorite { - err = m.FavoriteStore.SetTx(ctx, tx, permission.UserID(ctx), assignment.EscalationPolicyTarget(pol.ID)) + err = m.FavoriteStore.Set(ctx, tx, permission.UserID(ctx), assignment.EscalationPolicyTarget(pol.ID)) if err != nil { return err } diff --git a/graphql2/graphqlapp/mutation.go b/graphql2/graphqlapp/mutation.go index e03d6cbc5d..8d8a51b835 100644 --- a/graphql2/graphqlapp/mutation.go +++ b/graphql2/graphqlapp/mutation.go @@ -31,9 +31,9 @@ func (a *App) Mutation() graphql2.MutationResolver { return (*Mutation)(a) } func (a *Mutation) SetFavorite(ctx context.Context, input graphql2.SetFavoriteInput) (bool, error) { var err error if input.Favorite { - err = a.FavoriteStore.Set(ctx, permission.UserID(ctx), input.Target) + err = a.FavoriteStore.Set(ctx, a.DB, permission.UserID(ctx), input.Target) } else { - err = a.FavoriteStore.Unset(ctx, permission.UserID(ctx), input.Target) + err = a.FavoriteStore.Unset(ctx, a.DB, permission.UserID(ctx), input.Target) } if err != nil { @@ -269,7 +269,7 @@ func (a *Mutation) tryDeleteAll(ctx context.Context, input []assignment.RawTarge case assignment.TargetTypeRotation: err = errors.Wrap(a.RotationStore.DeleteManyTx(ctx, tx, ids), "delete rotations") case assignment.TargetTypeContactMethod: - err = errors.Wrap(a.CMStore.DeleteTx(ctx, tx, ids...), "delete contact methods") + err = errors.Wrap(a.CMStore.Delete(ctx, tx, ids...), "delete contact methods") case assignment.TargetTypeNotificationRule: err = errors.Wrap(a.NRStore.DeleteTx(ctx, tx, ids...), "delete notification rules") case assignment.TargetTypeHeartbeatMonitor: diff --git a/graphql2/graphqlapp/rotation.go b/graphql2/graphqlapp/rotation.go index 5cd60cdf79..a97b02a977 100644 --- a/graphql2/graphqlapp/rotation.go +++ b/graphql2/graphqlapp/rotation.go @@ -51,7 +51,7 @@ func (m *Mutation) CreateRotation(ctx context.Context, input graphql2.CreateRota } if input.Favorite != nil && *input.Favorite { - err = m.FavoriteStore.SetTx(ctx, tx, permission.UserID(ctx), assignment.RotationTarget(result.ID)) + err = m.FavoriteStore.Set(ctx, tx, permission.UserID(ctx), assignment.RotationTarget(result.ID)) if err != nil { return err } diff --git a/graphql2/graphqlapp/schedule.go b/graphql2/graphqlapp/schedule.go index 3e076fcc84..e5feb62751 100644 --- a/graphql2/graphqlapp/schedule.go +++ b/graphql2/graphqlapp/schedule.go @@ -248,7 +248,7 @@ func (m *Mutation) CreateSchedule(ctx context.Context, input graphql2.CreateSche return err } if input.Favorite != nil && *input.Favorite { - err = m.FavoriteStore.SetTx(ctx, tx, permission.UserID(ctx), assignment.ScheduleTarget(sched.ID)) + err = m.FavoriteStore.Set(ctx, tx, permission.UserID(ctx), assignment.ScheduleTarget(sched.ID)) if err != nil { return err } diff --git a/graphql2/graphqlapp/service.go b/graphql2/graphqlapp/service.go index df75621697..12a6ae23b9 100644 --- a/graphql2/graphqlapp/service.go +++ b/graphql2/graphqlapp/service.go @@ -162,7 +162,7 @@ func (m *Mutation) CreateService(ctx context.Context, input graphql2.CreateServi } if input.Favorite != nil && *input.Favorite { - err = m.FavoriteStore.SetTx(ctx, tx, permission.UserID(ctx), assignment.ServiceTarget(result.ID)) + err = m.FavoriteStore.Set(ctx, tx, permission.UserID(ctx), assignment.ServiceTarget(result.ID)) if err != nil { return err } diff --git a/graphql2/graphqlapp/user.go b/graphql2/graphqlapp/user.go index 0ef315fff8..8ff4710f09 100644 --- a/graphql2/graphqlapp/user.go +++ b/graphql2/graphqlapp/user.go @@ -67,7 +67,7 @@ func (a *User) Role(ctx context.Context, usr *user.User) (graphql2.UserRole, err } func (a *User) ContactMethods(ctx context.Context, obj *user.User) ([]contactmethod.ContactMethod, error) { - return a.CMStore.FindAll(ctx, obj.ID) + return a.CMStore.FindAll(ctx, a.DB, obj.ID) } func (a *User) NotificationRules(ctx context.Context, obj *user.User) ([]notificationrule.NotificationRule, error) { @@ -165,7 +165,7 @@ func (a *Mutation) CreateUser(ctx context.Context, input graphql2.CreateUserInpu return err } if input.Favorite != nil && *input.Favorite { - err = a.FavoriteStore.SetTx(ctx, tx, permission.UserID(ctx), assignment.UserTarget(newUser.ID)) + err = a.FavoriteStore.Set(ctx, tx, permission.UserID(ctx), assignment.UserTarget(newUser.ID)) if err != nil { return err } diff --git a/notification/twilio/carrier.go b/notification/twilio/carrier.go index d64c149233..052a01149d 100644 --- a/notification/twilio/carrier.go +++ b/notification/twilio/carrier.go @@ -36,7 +36,7 @@ var ErrCarrierStale = errors.New("carrier data is stale") var ErrCarrierUnavailable = errors.New("carrier data is unavailable") func (c *Config) dbCarrierInfo(ctx context.Context, number string) (*CarrierInfo, error) { - m, err := c.CMStore.MetadataByTypeValue(ctx, nil, contactmethod.TypeSMS, number) + m, err := c.CMStore.MetadataByTypeValue(ctx, c.DB, contactmethod.TypeSMS, number) if errors.Is(err, sql.ErrNoRows) { return nil, ErrCarrierUnavailable } @@ -129,11 +129,11 @@ func (c Config) FetchCarrierInfo(ctx context.Context, number string) (*CarrierIn m.CarrierV1.MobileCountryCode = result.Carrier.MobileCountryCode m.CarrierV1.MobileNetworkCode = result.Carrier.MobileNetworkCode - err = c.CMStore.SetCarrierV1MetadataByTypeValue(ctx, nil, contactmethod.TypeSMS, number, &m) + err = c.CMStore.SetCarrierV1MetadataByTypeValue(ctx, c.DB, contactmethod.TypeSMS, number, &m) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Log(ctx, err) } - err = c.CMStore.SetCarrierV1MetadataByTypeValue(ctx, nil, contactmethod.TypeVoice, number, &m) + err = c.CMStore.SetCarrierV1MetadataByTypeValue(ctx, c.DB, contactmethod.TypeVoice, number, &m) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Log(ctx, err) } diff --git a/notification/twilio/config.go b/notification/twilio/config.go index f4c32ee6c7..5f27d4affa 100644 --- a/notification/twilio/config.go +++ b/notification/twilio/config.go @@ -1,6 +1,7 @@ package twilio import ( + "database/sql" "net/http" "github.com/target/goalert/user/contactmethod" @@ -24,4 +25,7 @@ type Config struct { // CMStore is used for storing and fetching metadata (like carrier information). CMStore *contactmethod.Store + + // DB is used for storing DB connection data (needed for carrier metadata dbtx). + DB *sql.DB } diff --git a/override/queries.sql b/override/queries.sql index 587ddad5b8..3eb873132b 100644 --- a/override/queries.sql +++ b/override/queries.sql @@ -37,7 +37,7 @@ AND ( AND ( /* only include overrides that start before/within the search end */ sqlc.narg(search_end)::timestamptz ISNULL - OR o.start_time <= @search_start) + OR o.start_time <= @search_end) AND ( /* resume search after specified "cursor" override */ @after_id::uuid ISNULL diff --git a/package.json b/package.json index 8019abddd3..a4cda61ae6 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ ] }, "devDependencies": { - "@apollo/client": "3.8.4", - "@babel/core": "7.22.20", + "@apollo/client": "3.8.5", + "@babel/core": "7.23.0", "@babel/plugin-transform-modules-commonjs": "7.22.15", "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", @@ -44,7 +44,7 @@ "@mui/material": "5.14.10", "@mui/styles": "5.14.10", "@mui/system": "5.14.10", - "@mui/x-data-grid": "6.14.0", + "@mui/x-data-grid": "6.16.2", "@playwright/test": "1.38.0", "@types/chance": "1.1.4", "@types/diff": "5.0.5", @@ -56,7 +56,7 @@ "@types/react": "18.2.22", "@types/react-big-calendar": "1.6.5", "@types/react-dom": "18.2.7", - "@types/react-transition-group": "4.4.6", + "@types/react-transition-group": "4.4.7", "@types/react-virtualized-auto-sizer": "1.0.1", "@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/parser": "6.7.2", @@ -108,6 +108,7 @@ "redux": "4.2.1", "redux-devtools-extension": "2.13.9", "redux-thunk": "2.4.2", + "remark-breaks": "4.0.0", "remark-gfm": "3.0.1", "stylelint": "15.10.3", "stylelint-config-standard": "34.0.0", diff --git a/schedule/rotation/search.go b/schedule/rotation/search.go index 40cb4d180b..5e516cf434 100644 --- a/schedule/rotation/search.go +++ b/schedule/rotation/search.go @@ -57,7 +57,7 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers() AND NOT rot.id = any(:omit) {{end}} {{if .Search}} - AND {{orderedPrefixSearch "search" "rot.name"}} + AND ({{orderedPrefixSearch "search" "rot.name"}} OR {{contains "search" "rot.description"}}) {{end}} {{if .After.Name}} AND diff --git a/schedule/search.go b/schedule/search.go index f99cb1d2df..24e32e3a29 100644 --- a/schedule/search.go +++ b/schedule/search.go @@ -56,7 +56,7 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers() AND NOT sched.id = any(:omit) {{end}} {{if .Search}} - AND {{orderedPrefixSearch "search" "sched.name"}} + AND ({{orderedPrefixSearch "search" "sched.name"}} OR {{contains "search" "sched.description"}}) {{end}} {{if .After.Name}} AND diff --git a/search/render.go b/search/render.go index ac427d9435..2fcca0d6c6 100644 --- a/search/render.go +++ b/search/render.go @@ -26,6 +26,14 @@ func Helpers() template.FuncMap { "orderedPrefixSearch": func(argName string, columnName string) string { return fmt.Sprintf("lower(%s) ~ :~%s", columnName, argName) }, + "contains": func(argName string, columnName string) string { + // search for the term in the column + // + // - case insensitive + // - allow for partial matches + // - escape % and _ using `\` (backslash -- the default escape character) + return fmt.Sprintf(`%s ilike '%%' || REPLACE(REPLACE(REPLACE(:%s, '\', '\\'), '%%', '\%%'), '_', '\_') || '%%'`, columnName, argName) + }, "textSearch": func(argName string, columnNames ...string) string { var buf strings.Builder buf.WriteRune('(') diff --git a/service/search.go b/service/search.go index b2b00b0f36..66b8027ea8 100644 --- a/service/search.go +++ b/service/search.go @@ -78,7 +78,7 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers() ) {{end}} {{- if and .Search (not .LabelKey) (not .IntegrationKey)}} - AND {{orderedPrefixSearch "search" "svc.name"}} + AND ({{orderedPrefixSearch "search" "svc.name"}} OR {{contains "search" "svc.description"}}) {{- end}} {{- if .After.Name}} AND diff --git a/sqlc.yaml b/sqlc.yaml index 1bd54e3ef2..2dbc335482 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -25,6 +25,8 @@ sql: - auth/authlink/queries.sql - alert/alertlog/queries.sql - service/rule/queries.sql + - user/favorite/queries.sql + - user/contactmethod/queries.sql - integrationkey/queries.sql - apikey/queries.sql - signal/queries.sql diff --git a/user/contactmethod/queries.sql b/user/contactmethod/queries.sql new file mode 100644 index 0000000000..7c9880aa10 --- /dev/null +++ b/user/contactmethod/queries.sql @@ -0,0 +1,132 @@ +-- name: ContactMethodAdd :exec +INSERT INTO user_contact_methods(id, name, type, value, disabled, user_id, enable_status_updates) + VALUES ($1, $2, $3, $4, $5, $6, $7); + +-- name: ContactMethodUpdate :exec +UPDATE + user_contact_methods +SET + name = $2, + disabled = $3, + enable_status_updates = $4 +WHERE + id = $1; + +-- name: DeleteContactMethod :exec +DELETE FROM user_contact_methods +WHERE id = ANY ($1::uuid[]); + +-- name: ContactMethodFineOne :one +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = $1; + +-- name: ContactMethodFindOneUpdate :one +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = $1 +FOR UPDATE; + +-- name: ContactMethodFindMany :many +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + id = ANY ($1::uuid[]); + +-- name: ContactMethodFindAll :many +SELECT + id, + name, + type, + value, + disabled, + user_id, + last_test_verify_at, + enable_status_updates, + pending +FROM + user_contact_methods +WHERE + user_id = $1; + +-- name: ContactMethodLookupUserID :many +SELECT DISTINCT + user_id +FROM + user_contact_methods +WHERE + id = ANY ($1::uuid[]); + +-- name: ContactMethodEnable :one +UPDATE + user_contact_methods +SET + disabled = FALSE +WHERE + type = $1 + AND value = $2 +RETURNING + id; + +-- name: ContactMethodMetaTV :one +SELECT + coalesce(metadata, '{}'), + now()::timestamptz AS now +FROM + user_contact_methods +WHERE + type = $1 + AND value = $2; + +-- name: ContactMethodUpdateMetaTV :exec +UPDATE + user_contact_methods +SET + metadata = jsonb_set(jsonb_set(metadata, '{CarrierV1}', @carrier_v1::jsonb), '{CarrierV1,UpdatedAt}',('"' || NOW()::timestamptz AT TIME ZONE 'UTC' || '"')::jsonb) +WHERE + type = $1 + AND value = $2; + +-- name: ContactMethodDisable :one +UPDATE + user_contact_methods +SET + disabled = TRUE +WHERE + type = $1 + AND value = $2 +RETURNING + id; + diff --git a/user/contactmethod/store.go b/user/contactmethod/store.go index f597da64e8..1a4265a161 100644 --- a/user/contactmethod/store.go +++ b/user/contactmethod/store.go @@ -2,173 +2,61 @@ package contactmethod import ( "context" - "database/sql" "encoding/json" - "time" + "github.com/google/uuid" + "github.com/target/goalert/gadb" "github.com/target/goalert/permission" - "github.com/target/goalert/util" "github.com/target/goalert/util/log" - "github.com/target/goalert/util/sqlutil" "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" ) // Store implements the lookup and management of ContactMethods against a *sql.Store backend. type Store struct { - db *sql.DB - - insert *sql.Stmt - update *sql.Stmt - delete *sql.Stmt - findOne *sql.Stmt - findOneUpd *sql.Stmt - findMany *sql.Stmt - findAll *sql.Stmt - lookupUserID *sql.Stmt - enable *sql.Stmt - disable *sql.Stmt - metaTV *sql.Stmt - setMetaTV *sql.Stmt - now *sql.Stmt } -// NewStore will create a DB backend from a sql.DB. An error will be returned if statements fail to prepare. -func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { - p := &util.Prepare{DB: db, Ctx: ctx} - return &Store{ - db: db, - - now: p.P(`select now()`), - - metaTV: p.P(` - SELECT coalesce(metadata, '{}'), now() - FROM user_contact_methods - WHERE type = $1 AND value = $2 - `), - setMetaTV: p.P(` - UPDATE user_contact_methods - SET metadata = $3 - WHERE type = $1 AND value = $2 - `), - - enable: p.P(` - UPDATE user_contact_methods - SET disabled = false - WHERE type = $1 - AND value = $2 - RETURNING id - `), - disable: p.P(` - UPDATE user_contact_methods - SET disabled = true - WHERE type = $1 - AND value = $2 - RETURNING id - `), - lookupUserID: p.P(` - SELECT DISTINCT user_id - FROM user_contact_methods - WHERE id = any($1) - `), - insert: p.P(` - INSERT INTO user_contact_methods (id,name,type,value,disabled,user_id,enable_status_updates) - VALUES ($1,$2,$3,$4,$5,$6,$7) - `), - findOne: p.P(` - SELECT id,name,type,value,disabled,user_id,last_test_verify_at,enable_status_updates,pending - FROM user_contact_methods - WHERE id = $1 - `), - findOneUpd: p.P(` - SELECT id,name,type,value,disabled,user_id,last_test_verify_at,enable_status_updates,pending - FROM user_contact_methods - WHERE id = $1 - FOR UPDATE - `), - findMany: p.P(` - SELECT id,name,type,value,disabled,user_id,last_test_verify_at,enable_status_updates,pending - FROM user_contact_methods - WHERE id = any($1) - `), - findAll: p.P(` - SELECT id,name,type,value,disabled,user_id,last_test_verify_at,enable_status_updates,pending - FROM user_contact_methods - WHERE user_id = $1 - `), - update: p.P(` - UPDATE user_contact_methods - SET name = $2, disabled = $3, enable_status_updates = $4 - WHERE id = $1 - `), - delete: p.P(` - DELETE FROM user_contact_methods - WHERE id = any($1) - `), - }, p.Err -} - -func (s *Store) MetadataByTypeValue(ctx context.Context, tx *sql.Tx, typ Type, value string) (*Metadata, error) { +func (s *Store) MetadataByTypeValue(ctx context.Context, dbtx gadb.DBTX, typ Type, value string) (*Metadata, error) { err := permission.LimitCheckAny(ctx, permission.Admin) if err != nil { return nil, err } - var data json.RawMessage - var t time.Time - err = wrapTx(ctx, tx, s.metaTV).QueryRowContext(ctx, typ, value).Scan(&data, &t) + + data, err := gadb.New(dbtx).ContactMethodMetaTV(ctx, gadb.ContactMethodMetaTVParams{Type: gadb.EnumUserContactMethodType(typ), Value: value}) if err != nil { return nil, err } var m Metadata - err = json.Unmarshal(data, &m) + err = json.Unmarshal(data.Metadata, &m) if err != nil { return nil, err } - m.FetchedAt = t + m.FetchedAt = data.Now return &m, nil } -func (s *Store) SetCarrierV1MetadataByTypeValue(ctx context.Context, tx *sql.Tx, typ Type, value string, newM *Metadata) error { +func (s *Store) SetCarrierV1MetadataByTypeValue(ctx context.Context, dbtx gadb.DBTX, typ Type, value string, newM *Metadata) error { err := permission.LimitCheckAny(ctx, permission.Admin) if err != nil { return err } - var ownTx bool - if tx == nil { - tx, err = s.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer sqlutil.Rollback(ctx, "cm: set carrier metadata", tx) - ownTx = true - } - m, err := s.MetadataByTypeValue(ctx, tx, typ, value) + data, err := json.Marshal(newM.CarrierV1) if err != nil { return err } - m.CarrierV1 = newM.CarrierV1 - m.CarrierV1.UpdatedAt = m.FetchedAt - data, err := json.Marshal(m) + err = gadb.New(dbtx).ContactMethodUpdateMetaTV(ctx, gadb.ContactMethodUpdateMetaTVParams{Type: gadb.EnumUserContactMethodType(typ), Value: value, CarrierV1: data}) if err != nil { return err } - _, err = tx.StmtContext(ctx, s.setMetaTV).ExecContext(ctx, typ, value, data) - if err != nil { - return err - } - - if ownTx { - return tx.Commit() - } return nil } -func (s *Store) EnableByValue(ctx context.Context, t Type, v string) error { +func (s *Store) EnableByValue(ctx context.Context, dbtx gadb.DBTX, t Type, v string) error { err := permission.LimitCheckAny(ctx, permission.System) if err != nil { return err @@ -180,8 +68,7 @@ func (s *Store) EnableByValue(ctx context.Context, t Type, v string) error { return err } - var id string - err = s.enable.QueryRowContext(ctx, n.Type, n.Value).Scan(&id) + id, err := gadb.New(dbtx).ContactMethodEnable(ctx, gadb.ContactMethodEnableParams{Type: gadb.EnumUserContactMethodType(n.Type), Value: n.Value}) if err == nil { // NOTE: maintain a record of consent/dissent @@ -195,7 +82,7 @@ func (s *Store) EnableByValue(ctx context.Context, t Type, v string) error { return err } -func (s *Store) DisableByValue(ctx context.Context, t Type, v string) error { +func (s *Store) DisableByValue(ctx context.Context, dbtx gadb.DBTX, t Type, v string) error { err := permission.LimitCheckAny(ctx, permission.System) if err != nil { return err @@ -207,8 +94,7 @@ func (s *Store) DisableByValue(ctx context.Context, t Type, v string) error { return err } - var id string - err = s.disable.QueryRowContext(ctx, n.Type, n.Value).Scan(&id) + id, err := gadb.New(dbtx).ContactMethodDisable(ctx, gadb.ContactMethodDisableParams{Type: gadb.EnumUserContactMethodType(n.Type), Value: v}) if err == nil { // NOTE: maintain a record of consent/dissent @@ -223,7 +109,7 @@ func (s *Store) DisableByValue(ctx context.Context, t Type, v string) error { } // CreateTx inserts the new ContactMethod into the database. A new ID is always created. -func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) (*ContactMethod, error) { +func (s *Store) Create(ctx context.Context, dbtx gadb.DBTX, c *ContactMethod) (*ContactMethod, error) { err := permission.LimitCheckAny(ctx, permission.System, permission.Admin, permission.MatchUser(c.UserID)) if err != nil { return nil, err @@ -234,7 +120,15 @@ func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) (*Co return nil, err } - _, err = wrapTx(ctx, tx, s.insert).ExecContext(ctx, n.ID, n.Name, n.Type, n.Value, n.Disabled, n.UserID, n.StatusUpdates) + err = gadb.New(dbtx).ContactMethodAdd(ctx, gadb.ContactMethodAddParams{ + ID: uuid.MustParse(n.ID), + Name: n.Name, + Type: gadb.EnumUserContactMethodType(n.Type), + Value: n.Value, + Disabled: n.Disabled, + UserID: uuid.MustParse(n.UserID), + EnableStatusUpdates: n.StatusUpdates, + }) if err != nil { return nil, err } @@ -242,16 +136,8 @@ func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) (*Co return n, nil } -func wrapTx(ctx context.Context, tx *sql.Tx, stmt *sql.Stmt) *sql.Stmt { - if tx == nil { - return stmt - } - - return tx.StmtContext(ctx, stmt) -} - // Delete removes the ContactMethod from the database using the provided ID within a transaction. -func (s *Store) DeleteTx(ctx context.Context, tx *sql.Tx, ids ...string) error { +func (s *Store) Delete(ctx context.Context, dbtx gadb.DBTX, ids ...string) error { err := permission.LimitCheckAny(ctx, permission.Admin, permission.User) if err != nil { return err @@ -261,83 +147,69 @@ func (s *Store) DeleteTx(ctx context.Context, tx *sql.Tx, ids ...string) error { return nil } - err = validate.ManyUUID("ContactMethodID", ids, 50) + uids, err := validate.ParseManyUUID("ContactMethodID", ids, 50) if err != nil { return err } if permission.Admin(ctx) { - _, err = wrapTx(ctx, tx, s.delete).ExecContext(ctx, sqlutil.UUIDArray(ids)) + err = gadb.New(dbtx).DeleteContactMethod(ctx, uids) return err } - rows, err := wrapTx(ctx, tx, s.lookupUserID).QueryContext(ctx, sqlutil.UUIDArray(ids)) + rows, err := gadb.New(dbtx).ContactMethodLookupUserID(ctx, uids) if err != nil { return err } - defer rows.Close() var checks []permission.Checker - var userID string - for rows.Next() { - err = rows.Scan(&userID) - if err != nil { - return err - } - checks = append(checks, permission.MatchUser(userID)) + for _, id := range rows { + checks = append(checks, permission.MatchUser(id.String())) } err = permission.LimitCheckAny(ctx, checks...) if err != nil { return err } - _, err = wrapTx(ctx, tx, s.delete).ExecContext(ctx, sqlutil.UUIDArray(ids)) + + err = gadb.New(dbtx).DeleteContactMethod(ctx, uids) return err } // FindOneTx finds the contact method from the database using the provided ID within a transaction. -func (s *Store) FindOneTx(ctx context.Context, tx *sql.Tx, id string) (*ContactMethod, error) { +func (s *Store) FindOne(ctx context.Context, dbtx gadb.DBTX, id string) (*ContactMethod, error) { err := permission.LimitCheckAny(ctx, permission.All) if err != nil { return nil, err } - err = validate.UUID("ContactMethodID", id) - if err != nil { - return nil, err - } - var c ContactMethod - row := wrapTx(ctx, tx, s.findOneUpd).QueryRowContext(ctx, id) - err = row.Scan(&c.ID, &c.Name, &c.Type, &c.Value, &c.Disabled, &c.UserID, &c.lastTestVerifyAt, &c.StatusUpdates, &c.Pending) + methodUUID, err := validate.ParseUUID("ContactMethodID", id) if err != nil { return nil, err } - return &c, nil -} -// FindOne finds the contact method from the database using the provided ID. -func (s *Store) FindOne(ctx context.Context, id string) (*ContactMethod, error) { - err := validate.UUID("ContactMethodID", id) + row, err := gadb.New(dbtx).ContactMethodFindOneUpdate(ctx, methodUUID) if err != nil { return nil, err } - err = permission.LimitCheckAny(ctx, permission.All) - if err != nil { - return nil, err + c := ContactMethod{ + ID: row.ID.String(), + Name: row.Name, + Type: Type(row.Type), + Value: row.Value, + Disabled: row.Disabled, + UserID: row.UserID.String(), + Pending: row.Pending, + StatusUpdates: row.EnableStatusUpdates, + lastTestVerifyAt: row.LastTestVerifyAt, } - var c ContactMethod - row := s.findOne.QueryRowContext(ctx, id) - err = row.Scan(&c.ID, &c.Name, &c.Type, &c.Value, &c.Disabled, &c.UserID, &c.lastTestVerifyAt, &c.StatusUpdates, &c.Pending) - if err != nil { - return nil, err - } return &c, nil } // UpdateTx updates the contact method with the newly provided values within a transaction. -func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) error { +func (s *Store) Update(ctx context.Context, dbtx gadb.DBTX, c *ContactMethod) error { err := permission.LimitCheckAny(ctx, permission.Admin, permission.User) if err != nil { return err @@ -348,7 +220,7 @@ func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) erro return err } - cm, err := s.FindOneTx(ctx, tx, c.ID) + cm, err := s.FindOne(ctx, dbtx, c.ID) if err != nil { return err } @@ -363,7 +235,7 @@ func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) erro } if permission.Admin(ctx) { - _, err = wrapTx(ctx, tx, s.update).ExecContext(ctx, n.ID, n.Name, n.Disabled, n.StatusUpdates) + err = gadb.New(dbtx).ContactMethodUpdate(ctx, gadb.ContactMethodUpdateParams{ID: uuid.MustParse(n.ID), Name: n.Name, Disabled: n.Disabled, EnableStatusUpdates: n.StatusUpdates}) return err } @@ -372,13 +244,14 @@ func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, c *ContactMethod) erro return err } - _, err = wrapTx(ctx, tx, s.update).ExecContext(ctx, n.ID, n.Name, n.Disabled, n.StatusUpdates) + err = gadb.New(dbtx).ContactMethodUpdate(ctx, gadb.ContactMethodUpdateParams{ID: uuid.MustParse(n.ID), Name: n.Name, Disabled: n.Disabled, EnableStatusUpdates: n.StatusUpdates}) + return err } // FindMany will fetch all contact methods matching the given ids. -func (s *Store) FindMany(ctx context.Context, ids []string) ([]ContactMethod, error) { - err := validate.ManyUUID("ContactMethodID", ids, 50) +func (s *Store) FindMany(ctx context.Context, dbtx gadb.DBTX, ids []string) ([]ContactMethod, error) { + uids, err := validate.ParseManyUUID("ContactMethodID", ids, 50) if err != nil { return nil, err } @@ -388,32 +261,32 @@ func (s *Store) FindMany(ctx context.Context, ids []string) ([]ContactMethod, er return nil, err } - rows, err := s.findMany.QueryContext(ctx, sqlutil.UUIDArray(ids)) + rows, err := gadb.New(dbtx).ContactMethodFindMany(ctx, uids) if err != nil { return nil, err } - defer rows.Close() - - return scanAll(rows) -} -func scanAll(rows *sql.Rows) ([]ContactMethod, error) { - var contactMethods []ContactMethod - for rows.Next() { - var c ContactMethod - err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.Value, &c.Disabled, &c.UserID, &c.lastTestVerifyAt, &c.StatusUpdates, &c.Pending) - if err != nil { - return nil, err + cms := make([]ContactMethod, len(rows)) + for i, row := range rows { + cms[i] = ContactMethod{ + ID: row.ID.String(), + Name: row.Name, + Type: Type(row.Type), + Value: row.Value, + Disabled: row.Disabled, + UserID: row.UserID.String(), + Pending: row.Pending, + StatusUpdates: row.EnableStatusUpdates, + lastTestVerifyAt: row.LastTestVerifyAt, } - - contactMethods = append(contactMethods, c) } - return contactMethods, nil + + return cms, nil } // FindAll finds all contact methods from the database associated with the given user ID. -func (s *Store) FindAll(ctx context.Context, userID string) ([]ContactMethod, error) { - err := validate.UUID("UserID", userID) +func (s *Store) FindAll(ctx context.Context, dbtx gadb.DBTX, userID string) ([]ContactMethod, error) { + uid, err := validate.ParseUUID("ContactMethodID", userID) if err != nil { return nil, err } @@ -423,11 +296,25 @@ func (s *Store) FindAll(ctx context.Context, userID string) ([]ContactMethod, er return nil, err } - rows, err := s.findAll.QueryContext(ctx, userID) + rows, err := gadb.New(dbtx).ContactMethodFindAll(ctx, uid) if err != nil { return nil, err } - defer rows.Close() - return scanAll(rows) + cms := make([]ContactMethod, len(rows)) + for i, row := range rows { + cms[i] = ContactMethod{ + ID: row.ID.String(), + Name: row.Name, + Type: Type(row.Type), + Value: row.Value, + Disabled: row.Disabled, + UserID: row.UserID.String(), + Pending: row.Pending, + StatusUpdates: row.EnableStatusUpdates, + lastTestVerifyAt: row.LastTestVerifyAt, + } + } + + return cms, nil } diff --git a/user/favorite/queries.sql b/user/favorite/queries.sql new file mode 100644 index 0000000000..8f0b28f601 --- /dev/null +++ b/user/favorite/queries.sql @@ -0,0 +1,37 @@ +-- name: UserFavSet :exec +INSERT INTO user_favorites(user_id, tgt_service_id, tgt_schedule_id, tgt_rotation_id, tgt_escalation_policy_id, tgt_user_id) + VALUES ($1, $2, $3, $4, $5, $6) +ON CONFLICT + DO NOTHING; + +-- name: UserFavUnset :exec +DELETE FROM user_favorites +WHERE user_id = $1 + AND tgt_service_id = $2 + OR tgt_schedule_id = $3 + OR tgt_rotation_id = $4 + OR tgt_escalation_policy_id = $5 + OR tgt_user_id = $6; + +-- name: UserFavFindAll :many +SELECT + tgt_service_id, + tgt_schedule_id, + tgt_rotation_id, + tgt_escalation_policy_id, + tgt_user_id +FROM + user_favorites +WHERE + user_id = @user_id + AND ((tgt_service_id NOTNULL + AND @allow_services::bool) + OR (tgt_schedule_id NOTNULL + AND @allow_schedules::bool) + OR (tgt_rotation_id NOTNULL + AND @allow_rotations::bool) + OR (tgt_escalation_policy_id NOTNULL + AND @allow_escalation_policies::bool) + OR (tgt_user_id NOTNULL + AND @allow_users::bool)); + diff --git a/user/favorite/store.go b/user/favorite/store.go index 32fee7fec1..991d3eaffa 100644 --- a/user/favorite/store.go +++ b/user/favorite/store.go @@ -3,78 +3,26 @@ package favorite import ( "context" "database/sql" + "fmt" + "slices" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/target/goalert/assignment" + "github.com/target/goalert/gadb" "github.com/target/goalert/permission" - "github.com/target/goalert/util" "github.com/target/goalert/validation/validate" ) // Store allows the lookup and management of Favorites. -type Store struct { - db *sql.DB +type Store struct{} - insert *sql.Stmt - delete *sql.Stmt - findAll *sql.Stmt -} - -// NewStore will create a DB backend from a sql.DB. An error will be returned if statements fail to prepare. -func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { - p := &util.Prepare{DB: db, Ctx: ctx} - return &Store{ - db: db, - insert: p.P(` - INSERT INTO user_favorites ( - user_id, tgt_service_id, - tgt_schedule_id, - tgt_rotation_id, - tgt_escalation_policy_id, - tgt_user_id - ) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT DO NOTHING - `), - delete: p.P(` - DELETE FROM user_favorites - WHERE - user_id = $1 AND - tgt_service_id = $2 OR - tgt_schedule_id = $3 OR - tgt_rotation_id = $4 OR - tgt_escalation_policy_id = $5 OR - tgt_user_id = $6 - `), - findAll: p.P(` - SELECT - tgt_service_id, - tgt_schedule_id, - tgt_rotation_id, - tgt_escalation_policy_id, - tgt_user_id - FROM user_favorites - WHERE user_id = $1 - AND ( - (tgt_service_id NOTNULL AND $2) OR - (tgt_schedule_id NOTNULL AND $3) OR - (tgt_rotation_id NOTNULL AND $4) OR - (tgt_escalation_policy_id NOTNULL AND $5) OR - (tgt_user_id NOTNULL AND $6) - ) - `), - }, p.Err -} +// NewStore will create a new Store. +func NewStore(ctx context.Context) (*Store, error) { return &Store{}, nil } // Set will store the target as a favorite of the given user. Must be authorized as System or the same user. // It is safe to call multiple times. -func (s *Store) Set(ctx context.Context, userID string, tgt assignment.Target) error { - return s.SetTx(ctx, nil, userID, tgt) -} - -// SetTx will store the target as a favorite of the given user. Must be authorized as System or the same user. -// It is safe to call multiple times. -func (s *Store) SetTx(ctx context.Context, tx *sql.Tx, userID string, tgt assignment.Target) error { +func (s *Store) Set(ctx context.Context, tx gadb.DBTX, userID string, tgt assignment.Target) error { err := permission.LimitCheckAny(ctx, permission.System, permission.MatchUser(userID)) if err != nil { return err @@ -88,32 +36,24 @@ func (s *Store) SetTx(ctx context.Context, tx *sql.Tx, userID string, tgt assign if err != nil { return err } - stmt := s.insert - if tx != nil { - stmt = tx.StmtContext(ctx, stmt) - } - var serviceID, scheduleID, rotationID, epID, usrID sql.NullString + args := gadb.UserFavSetParams{UserID: uuid.MustParse(userID)} switch tgt.TargetType() { case assignment.TargetTypeService: - serviceID.Valid = true - serviceID.String = tgt.TargetID() + args.TgtServiceID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeSchedule: - scheduleID.Valid = true - scheduleID.String = tgt.TargetID() + args.TgtScheduleID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeRotation: - rotationID.Valid = true - rotationID.String = tgt.TargetID() + args.TgtRotationID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeEscalationPolicy: - epID.Valid = true - epID.String = tgt.TargetID() + args.TgtEscalationPolicyID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeUser: - usrID.Valid = true - usrID.String = tgt.TargetID() + args.TgtUserID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} } - _, err = stmt.ExecContext(ctx, userID, serviceID, scheduleID, rotationID, epID, usrID) + + err = gadb.New(tx).UserFavSet(ctx, args) if err != nil { - return errors.Wrap(err, "set favorite") + return fmt.Errorf("set favorite: %w", err) } return nil @@ -121,7 +61,7 @@ func (s *Store) SetTx(ctx context.Context, tx *sql.Tx, userID string, tgt assign // Unset will remove the target as a favorite of the given user. Must be authorized as System or the same user. // It is safe to call multiple times. -func (s *Store) Unset(ctx context.Context, userID string, tgt assignment.Target) error { +func (s *Store) Unset(ctx context.Context, tx gadb.DBTX, userID string, tgt assignment.Target) error { err := permission.LimitCheckAny(ctx, permission.System, permission.MatchUser(userID)) if err != nil { return err @@ -136,37 +76,34 @@ func (s *Store) Unset(ctx context.Context, userID string, tgt assignment.Target) if err != nil { return err } - var serviceID, scheduleID, rotationID, epID, usrID sql.NullString + + args := gadb.UserFavUnsetParams{UserID: uuid.MustParse(userID)} switch tgt.TargetType() { case assignment.TargetTypeService: - serviceID.Valid = true - serviceID.String = tgt.TargetID() + args.TgtServiceID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeSchedule: - scheduleID.Valid = true - scheduleID.String = tgt.TargetID() + args.TgtScheduleID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeRotation: - rotationID.Valid = true - rotationID.String = tgt.TargetID() + args.TgtRotationID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeEscalationPolicy: - epID.Valid = true - epID.String = tgt.TargetID() + args.TgtEscalationPolicyID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} case assignment.TargetTypeUser: - usrID.Valid = true - usrID.String = tgt.TargetID() + args.TgtUserID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(tgt.TargetID())} } - _, err = s.delete.ExecContext(ctx, userID, serviceID, scheduleID, rotationID, epID, usrID) + + err = gadb.New(tx).UserFavUnset(ctx, args) if errors.Is(err, sql.ErrNoRows) { // ignoring since it is safe to unset favorite (with retries) err = nil } if err != nil { - return err + return fmt.Errorf("unset favorite: %w", err) } return nil } -func (s *Store) FindAll(ctx context.Context, userID string, filter []assignment.TargetType) ([]assignment.Target, error) { +func (s *Store) FindAll(ctx context.Context, tx gadb.DBTX, userID string, filter []assignment.TargetType) ([]assignment.Target, error) { err := permission.LimitCheckAny(ctx, permission.System, permission.MatchUser(userID)) if err != nil { return nil, err @@ -180,55 +117,38 @@ func (s *Store) FindAll(ctx context.Context, userID string, filter []assignment. return nil, err } - var allowServices, allowSchedules, allowRotations, allowEscalationPolicies, allowUsers bool - if len(filter) == 0 { - allowServices = true - } else { - for _, f := range filter { - switch f { - case assignment.TargetTypeService: - allowServices = true - case assignment.TargetTypeSchedule: - allowSchedules = true - case assignment.TargetTypeRotation: - allowRotations = true - case assignment.TargetTypeEscalationPolicy: - allowEscalationPolicies = true - case assignment.TargetTypeUser: - allowUsers = true - } - } + args := gadb.UserFavFindAllParams{ + UserID: uuid.MustParse(userID), + AllowServices: len(filter) == 0 || slices.Contains(filter, assignment.TargetTypeService), + AllowSchedules: len(filter) == 0 || slices.Contains(filter, assignment.TargetTypeSchedule), + AllowRotations: len(filter) == 0 || slices.Contains(filter, assignment.TargetTypeRotation), + AllowEscalationPolicies: len(filter) == 0 || slices.Contains(filter, assignment.TargetTypeEscalationPolicy), + AllowUsers: len(filter) == 0 || slices.Contains(filter, assignment.TargetTypeUser), } - rows, err := s.findAll.QueryContext(ctx, userID, allowServices, allowSchedules, allowRotations, allowEscalationPolicies, allowUsers) + favs, err := gadb.New(tx).UserFavFindAll(ctx, args) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { - return nil, err + return nil, fmt.Errorf("find all favorites: %w", err) } - defer rows.Close() - - var targets []assignment.Target - for rows.Next() { - var svc, sched, rot, escpolicy, usr sql.NullString - err = rows.Scan(&svc, &sched, &rot, &escpolicy, &usr) - if err != nil { - return nil, err - } + targets := make([]assignment.Target, 0, len(favs)) + for _, fav := range favs { switch { - case svc.Valid: - targets = append(targets, assignment.ServiceTarget(svc.String)) - case sched.Valid: - targets = append(targets, assignment.ScheduleTarget(sched.String)) - case rot.Valid: - targets = append(targets, assignment.RotationTarget(rot.String)) - case escpolicy.Valid: - targets = append(targets, assignment.EscalationPolicyTarget(escpolicy.String)) - case usr.Valid: - targets = append(targets, assignment.UserTarget(usr.String)) + case fav.TgtServiceID.Valid: + targets = append(targets, assignment.ServiceTarget(fav.TgtServiceID.UUID.String())) + case fav.TgtScheduleID.Valid: + targets = append(targets, assignment.ScheduleTarget(fav.TgtScheduleID.UUID.String())) + case fav.TgtRotationID.Valid: + targets = append(targets, assignment.RotationTarget(fav.TgtRotationID.UUID.String())) + case fav.TgtEscalationPolicyID.Valid: + targets = append(targets, assignment.EscalationPolicyTarget(fav.TgtEscalationPolicyID.UUID.String())) + case fav.TgtUserID.Valid: + targets = append(targets, assignment.UserTarget(fav.TgtUserID.UUID.String())) } } + return targets, nil } diff --git a/web/src/app/admin/admin-service-metrics/useServiceMetrics.ts b/web/src/app/admin/admin-service-metrics/useServiceMetrics.ts new file mode 100644 index 0000000000..4b1221b0e3 --- /dev/null +++ b/web/src/app/admin/admin-service-metrics/useServiceMetrics.ts @@ -0,0 +1,77 @@ +import { IntegrationKeyType, Service, TargetType } from '../../../schema' + +export type TargetMetrics = { + [type in IntegrationKeyType | TargetType]: number +} +export type ServiceMetrics = { + keyTgtTotals: TargetMetrics + stepTgtTotals: TargetMetrics + filteredServices: Service[] +} +export type ServiceMetricFilters = { + labelKey?: string + labelValue?: string + epStepTgts?: string[] + intKeyTgts?: string[] +} + +export type ServiceMetricOpts = { + services: Service[] + filters: ServiceMetricFilters +} +export function useServiceMetrics(opts: ServiceMetricOpts): ServiceMetrics { + const { services, filters } = opts + + const filterServices = ( + services: Service[], + filters: ServiceMetricFilters, + ): Service[] => { + return services.filter((svc) => { + if (filters.labelKey) { + const labelMatch = svc.labels.some( + (label) => + filters.labelKey === label.key && + (!filters.labelValue || filters.labelValue === label.value), + ) + if (!labelMatch) return false + } + if (filters.epStepTgts?.length) { + const stepTargetMatch = svc.escalationPolicy?.steps.some((step) => + step.targets.some((tgt) => filters.epStepTgts?.includes(tgt.type)), + ) + if (!stepTargetMatch) return false + } + if (filters.intKeyTgts?.length) { + const intKeyMatch = svc.integrationKeys.some( + (key) => filters.intKeyTgts?.includes(key.type), + ) + if (!intKeyMatch) return false + } + return true + }) + } + + const calculateMetrics = (filteredServices: Service[]): ServiceMetrics => { + const metrics = { + keyTgtTotals: {}, + stepTgtTotals: {}, + } as ServiceMetrics + filteredServices.forEach((svc) => { + svc.escalationPolicy?.steps.forEach((step) => { + step.targets.forEach((tgt) => { + metrics.stepTgtTotals[tgt.type] = + (metrics.stepTgtTotals[tgt.type] || 0) + 1 + }) + }) + svc.integrationKeys.forEach((key) => { + metrics.keyTgtTotals[key.type] = + (metrics.keyTgtTotals[key.type] || 0) + 1 + }) + }) + return metrics + } + + const filteredServices = filterServices(services, filters) + const metrics = calculateMetrics(filteredServices) + return { ...metrics, filteredServices } +} diff --git a/web/src/app/admin/admin-service-metrics/useServices.ts b/web/src/app/admin/admin-service-metrics/useServices.ts new file mode 100644 index 0000000000..78df419cfc --- /dev/null +++ b/web/src/app/admin/admin-service-metrics/useServices.ts @@ -0,0 +1,136 @@ +import _ from 'lodash' +import React, { useLayoutEffect, useEffect, useRef, useState } from 'react' +import { gql, useClient } from 'urql' +import { Service } from '../../../schema' + +const servicesQuery = gql` + query services($input: ServiceSearchOptions!) { + services(input: $input) { + nodes { + id + name + onCallUsers { + userID + } + escalationPolicy { + id + name + steps { + targets { + name + type + } + } + } + integrationKeys { + type + name + } + heartbeatMonitors { + name + timeoutMinutes + lastHeartbeat + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +` + +const QUERY_LIMIT = 10 + +export type ServiceData = { + services: Service[] + loading: boolean + error: Error | undefined +} + +export function useServices(depKey: string, pause?: boolean): ServiceData { + const [services, setServices] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState() + const key = useRef(depKey) + key.current = depKey + + useEffect(() => { + return () => { + // cancel on unmount + key.current = '' + } + }, []) + + const client = useClient() + const fetch = React.useCallback(async () => { + setServices([]) + setLoading(true) + setError(undefined) + if (pause) { + return + } + async function fetchServices( + cursor: string, + ): Promise<[Service[], boolean, string, Error | undefined]> { + const q = await client + .query(servicesQuery, { + input: { + first: QUERY_LIMIT, + after: cursor, + }, + }) + .toPromise() + + if (q.error) { + return [[], false, '', q.error] + } + + return [ + q.data.services.nodes, + q.data.services.pageInfo.hasNextPage, + q.data.services.pageInfo.endCursor, + undefined, + ] + } + + const throttledSetServices = _.throttle( + (services, loading) => { + setServices(services) + setLoading(loading) + }, + 3000, + { leading: true }, + ) + + let endCursor = '' + let hasNextPage = true + let error = null + let services = [] + let allServices: Service[] = [] + while (hasNextPage) { + ;[services, hasNextPage, endCursor, error] = + await fetchServices(endCursor) + if (key.current !== depKey) return // abort if the key has changed + if (error) { + setError(error) + throttledSetServices.cancel() + return + } + allServices = allServices.concat(services) + throttledSetServices(allServices, true) + } + + throttledSetServices(allServices, false) + }, [depKey, pause]) + + useLayoutEffect(() => { + fetch() + }, [depKey, pause]) + + return { + services, + loading, + error, + } +} diff --git a/web/src/app/documentation/Documentation.tsx b/web/src/app/documentation/Documentation.tsx index 8edcdb0df4..1300746ab6 100644 --- a/web/src/app/documentation/Documentation.tsx +++ b/web/src/app/documentation/Documentation.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' @@ -23,25 +23,36 @@ export default function Documentation(): JSX.Element { const classes = useStyles() // NOTE list markdown documents here - let markdownDocs = [integrationKeys] + let markdownDocs = [{ doc: integrationKeys, id: 'integration-keys' }] if (webhookEnabled) { - markdownDocs.push(webhooks) + markdownDocs.push({ doc: webhooks, id: 'webhooks' }) } - markdownDocs = markdownDocs.map((md) => - md.replaceAll( + markdownDocs = markdownDocs.map((md) => ({ + id: md.id, + doc: md.doc.replaceAll( 'https://', publicURL || `${window.location.origin}${pathPrefix}`, ), - ) + })) + + // useEffect to ensure that the page scrolls to the correct section after rendering + useEffect(() => { + const hash = window.location.hash + if (!hash) return + const el = document.getElementById(hash.slice(1)) + if (!el) return + + el.scrollIntoView() + }, [webhookEnabled, publicURL]) return ( - {markdownDocs.map((doc, i) => ( - + {markdownDocs.map((md, i) => ( + - + diff --git a/web/src/app/escalation-policies/PolicyStepForm.js b/web/src/app/escalation-policies/PolicyStepForm.js index 337b943a3f..b6c2bee5b7 100644 --- a/web/src/app/escalation-policies/PolicyStepForm.js +++ b/web/src/app/escalation-policies/PolicyStepForm.js @@ -26,6 +26,7 @@ import { import { SlackBW as SlackIcon } from '../icons/components/Icons' import { Config } from '../util/RequireConfig' import NumberField from '../util/NumberField' +import AppLink from '../util/AppLink' const useStyles = makeStyles({ badge: { @@ -252,6 +253,11 @@ function PolicyStepForm(props) { name='webhooks' mapValue={getTargetsByType('chanWebhook')} mapOnChangeValue={setTargetType('chanWebhook')} + hint={ + + Webhook Documentation + + } /> diff --git a/web/src/app/forms/FormField.js b/web/src/app/forms/FormField.js index 541dbf9eaa..3d2e6816ee 100644 --- a/web/src/app/forms/FormField.js +++ b/web/src/app/forms/FormField.js @@ -158,7 +158,7 @@ export function FormField(props) { if (count) { return charCountWrapper({hint}, count) } - return {hint} + return {hint} } return null diff --git a/web/src/app/rotations/HandoffSummary.tsx b/web/src/app/rotations/HandoffSummary.tsx index 4234a6b04b..2951142d46 100644 --- a/web/src/app/rotations/HandoffSummary.tsx +++ b/web/src/app/rotations/HandoffSummary.tsx @@ -13,7 +13,7 @@ function dur(p: HandoffSummaryProps): JSX.Element | string { if (p.type === 'hourly') return