mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-01-06 00:40:10 -06:00
Enhancement webhook usability (#807)
* feat: secret copier * feat: improved form * fix: quotes * wip: improved flow * feat: health check logging * fix: page design * fix: hard delete, no upsert * fix: reset modal state * fix: empty text * fix: worker state * fix: update only token * fix: dont delete name * fix: logs component * fix: sort order * chore: build * fix: webhook worker cleanup * chore: squash migrations * Update api-contracts/openapi/paths/webhook-worker/webhook-worker.yaml Co-authored-by: abelanger5 <belanger@sas.upenn.edu> * chore: rename * fix: wrong query --------- Co-authored-by: abelanger5 <belanger@sas.upenn.edu>
This commit is contained in:
@@ -234,6 +234,12 @@ WorkflowMetrics:
|
||||
$ref: "./workflow.yaml#/WorkflowMetrics"
|
||||
WebhookWorker:
|
||||
$ref: "./webhook_worker.yaml#/WebhookWorker"
|
||||
WebhookWorkerRequestMethod:
|
||||
$ref: "./webhook_worker.yaml#/WebhookWorkerRequestMethod"
|
||||
WebhookWorkerRequest:
|
||||
$ref: "./webhook_worker.yaml#/WebhookWorkerRequest"
|
||||
WebhookWorkerRequestListResponse:
|
||||
$ref: "./webhook_worker.yaml#/WebhookWorkerRequestListResponse"
|
||||
WebhookWorkerCreated:
|
||||
$ref: "./webhook_worker.yaml#/WebhookWorkerCreated"
|
||||
WebhookWorkerCreateRequest:
|
||||
|
||||
@@ -14,6 +14,39 @@ WebhookWorker:
|
||||
- url
|
||||
type: object
|
||||
|
||||
WebhookWorkerRequestMethod:
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
|
||||
WebhookWorkerRequest:
|
||||
properties:
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: The date and time the request was created.
|
||||
method:
|
||||
$ref: "#/WebhookWorkerRequestMethod"
|
||||
description: The HTTP method used for the request.
|
||||
statusCode:
|
||||
type: integer
|
||||
description: The HTTP status code of the response.
|
||||
required:
|
||||
- created_at
|
||||
- method
|
||||
- statusCode
|
||||
type: object
|
||||
|
||||
WebhookWorkerRequestListResponse:
|
||||
properties:
|
||||
requests:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/WebhookWorkerRequest"
|
||||
description: The list of webhook requests.
|
||||
type: object
|
||||
|
||||
WebhookWorkerCreated:
|
||||
properties:
|
||||
metadata:
|
||||
|
||||
@@ -140,6 +140,10 @@ Worker:
|
||||
webhookUrl:
|
||||
type: string
|
||||
description: The webhook URL for the worker.
|
||||
webhookId:
|
||||
type: string
|
||||
description: The webhook ID for the worker.
|
||||
format: uuid
|
||||
required:
|
||||
- metadata
|
||||
- name
|
||||
|
||||
@@ -148,5 +148,7 @@ paths:
|
||||
$ref: "./paths/webhook-worker/webhook-worker.yaml#/webhookworkers"
|
||||
/api/v1/webhook-workers/{webhook}:
|
||||
$ref: "./paths/webhook-worker/webhook-worker.yaml#/webhookworker"
|
||||
/api/v1/webhook-workers/{webhook}/requests:
|
||||
$ref: "./paths/webhook-worker/webhook-worker.yaml#/webhookworkerRequests"
|
||||
/api/v1/workflow-runs/{workflow-run}/input:
|
||||
$ref: "./paths/workflow-run/workflow-run.yaml#/getWorkflowRunInput"
|
||||
|
||||
@@ -121,3 +121,45 @@ webhookworker:
|
||||
schema:
|
||||
$ref: "../../components/schemas/_index.yaml#/APIErrors"
|
||||
description: Method not allowed
|
||||
|
||||
webhookworkerRequests:
|
||||
get:
|
||||
description: Lists all requests for a webhook
|
||||
summary: List webhook requests
|
||||
operationId: webhook-requests:list
|
||||
x-resources: ["tenant", "webhook"]
|
||||
parameters:
|
||||
- description: The webhook id
|
||||
in: path
|
||||
name: webhook
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
minLength: 36
|
||||
maxLength: 36
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "../../components/schemas/_index.yaml#/WebhookWorkerRequestListResponse"
|
||||
description: The list of webhook requests
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "../../components/schemas/_index.yaml#/APIErrors"
|
||||
description: A malformed or bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "../../components/schemas/_index.yaml#/APIErrors"
|
||||
description: Unauthorized
|
||||
"405":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "../../components/schemas/_index.yaml#/APIErrors"
|
||||
description: Method not allowed
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package webhookworker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/apierrors"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers"
|
||||
"github.com/hatchet-dev/hatchet/pkg/random"
|
||||
@@ -29,13 +32,20 @@ func (i *WebhookWorkersService) WebhookCreate(ctx echo.Context, request gen.Webh
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ww, err := i.config.EngineRepository.WebhookWorker().UpsertWebhookWorker(ctx.Request().Context(), &repository.UpsertWebhookWorkerOpts{
|
||||
ww, err := i.config.EngineRepository.WebhookWorker().CreateWebhookWorker(ctx.Request().Context(), &repository.CreateWebhookWorkerOpts{
|
||||
TenantId: tenant.ID,
|
||||
Name: request.Body.Name,
|
||||
URL: request.Body.Url,
|
||||
Secret: encSecret,
|
||||
Deleted: repository.BoolPtr(false),
|
||||
})
|
||||
|
||||
if errors.Is(err, repository.ErrDuplicateKey) {
|
||||
return gen.WebhookCreate400JSONResponse(
|
||||
apierrors.NewAPIErrors("A webhook with the same url already exists, please delete it and try again.", "url"),
|
||||
), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ func (i *WebhookWorkersService) WebhookDelete(ctx echo.Context, request gen.Webh
|
||||
tenant := ctx.Get("tenant").(*db.TenantModel)
|
||||
webhook := ctx.Get("webhook").(*db.WebhookWorkerModel)
|
||||
|
||||
err := i.config.EngineRepository.WebhookWorker().DeleteWebhookWorker(ctx.Request().Context(), webhook.ID, tenant.ID)
|
||||
err := i.config.EngineRepository.WebhookWorker().SoftDeleteWebhookWorker(ctx.Request().Context(), webhook.ID, tenant.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
34
api/v1/server/handlers/webhook-worker/list_requests.go
Normal file
34
api/v1/server/handlers/webhook-worker/list_requests.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package webhookworker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers"
|
||||
)
|
||||
|
||||
func (i *WebhookWorkersService) WebhookRequestsList(ctx echo.Context, request gen.WebhookRequestsListRequestObject) (gen.WebhookRequestsListResponseObject, error) {
|
||||
dbCtx, cancel := context.WithTimeout(ctx.Request().Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
requests, err := i.config.EngineRepository.WebhookWorker().ListWebhookWorkerRequests(dbCtx, request.Webhook.String())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows := make([]gen.WebhookWorkerRequest, len(requests))
|
||||
|
||||
for i := range requests {
|
||||
rows[i] = *transformers.ToWebhookWorkerRequest(requests[i])
|
||||
}
|
||||
|
||||
return gen.WebhookRequestsList200JSONResponse(
|
||||
gen.WebhookWorkerRequestListResponse{
|
||||
Requests: &rows,
|
||||
},
|
||||
), nil
|
||||
}
|
||||
@@ -27,6 +27,15 @@ func (t *WorkerService) WorkerGet(ctx echo.Context, request gen.WorkerGetRequest
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actions, err := t.config.APIRepository.Worker().GetWorkerActionsByWorkerId(
|
||||
sqlchelpers.UUIDToStr(worker.Worker.TenantId),
|
||||
sqlchelpers.UUIDToStr(worker.Worker.ID),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respStepRuns := make([]gen.RecentStepRuns, len(recent))
|
||||
|
||||
for i := range recent {
|
||||
@@ -39,7 +48,9 @@ func (t *WorkerService) WorkerGet(ctx echo.Context, request gen.WorkerGetRequest
|
||||
respStepRuns[i] = *genStepRun
|
||||
}
|
||||
|
||||
workerResp := *transformers.ToWorkerSqlc(&worker.Worker, nil, &worker.WebhookUrl.String)
|
||||
slots := int(worker.FilledSlots)
|
||||
|
||||
workerResp := *transformers.ToWorkerSqlc(&worker.Worker, &slots, &worker.WebhookUrl.String, actions)
|
||||
|
||||
workerResp.RecentStepRuns = &respStepRuns
|
||||
workerResp.Slots = transformers.ToSlotState(slotState)
|
||||
|
||||
@@ -30,7 +30,7 @@ func (t *WorkerService) WorkerList(ctx echo.Context, request gen.WorkerListReque
|
||||
workerCp := worker
|
||||
slots := int(worker.Slots)
|
||||
|
||||
rows[i] = *transformers.ToWorkerSqlc(&workerCp.Worker, &slots, &workerCp.WebhookUrl.String)
|
||||
rows[i] = *transformers.ToWorkerSqlc(&workerCp.Worker, &slots, &workerCp.WebhookUrl.String, nil)
|
||||
}
|
||||
|
||||
return gen.WorkerList200JSONResponse(
|
||||
|
||||
@@ -35,5 +35,5 @@ func (t *WorkerService) WorkerUpdate(ctx echo.Context, request gen.WorkerUpdateR
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gen.WorkerUpdate200JSONResponse(*transformers.ToWorkerSqlc(updatedWorker, nil, nil)), nil
|
||||
return gen.WorkerUpdate200JSONResponse(*transformers.ToWorkerSqlc(updatedWorker, nil, nil, nil)), nil
|
||||
}
|
||||
|
||||
@@ -966,6 +966,25 @@ type WebhookWorkerListResponse struct {
|
||||
Rows *[]WebhookWorker `json:"rows,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequest defines model for WebhookWorkerRequest.
|
||||
type WebhookWorkerRequest struct {
|
||||
// CreatedAt The date and time the request was created.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Method WebhookWorkerRequestMethod `json:"method"`
|
||||
|
||||
// StatusCode The HTTP status code of the response.
|
||||
StatusCode int `json:"statusCode"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequestListResponse defines model for WebhookWorkerRequestListResponse.
|
||||
type WebhookWorkerRequestListResponse struct {
|
||||
// Requests The list of webhook requests.
|
||||
Requests *[]WebhookWorkerRequest `json:"requests,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequestMethod defines model for WebhookWorkerRequestMethod.
|
||||
type WebhookWorkerRequestMethod = interface{}
|
||||
|
||||
// Worker defines model for Worker.
|
||||
type Worker struct {
|
||||
// Actions The actions this worker can perform.
|
||||
@@ -1003,6 +1022,9 @@ type Worker struct {
|
||||
Status *WorkerStatus `json:"status,omitempty"`
|
||||
Type WorkerType `json:"type"`
|
||||
|
||||
// WebhookId The webhook ID for the worker.
|
||||
WebhookId *openapi_types.UUID `json:"webhookId,omitempty"`
|
||||
|
||||
// WebhookUrl The webhook URL for the worker.
|
||||
WebhookUrl *string `json:"webhookUrl,omitempty"`
|
||||
}
|
||||
@@ -1655,6 +1677,9 @@ type ServerInterface interface {
|
||||
// Delete a webhook
|
||||
// (DELETE /api/v1/webhook-workers/{webhook})
|
||||
WebhookDelete(ctx echo.Context, webhook openapi_types.UUID) error
|
||||
// List webhook requests
|
||||
// (GET /api/v1/webhook-workers/{webhook}/requests)
|
||||
WebhookRequestsList(ctx echo.Context, webhook openapi_types.UUID) error
|
||||
// Get worker
|
||||
// (GET /api/v1/workers/{worker})
|
||||
WorkerGet(ctx echo.Context, worker openapi_types.UUID, params WorkerGetParams) error
|
||||
@@ -3184,6 +3209,26 @@ func (w *ServerInterfaceWrapper) WebhookDelete(ctx echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// WebhookRequestsList converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) WebhookRequestsList(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "webhook" -------------
|
||||
var webhook openapi_types.UUID
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "webhook", runtime.ParamLocationPath, ctx.Param("webhook"), &webhook)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter webhook: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set(BearerAuthScopes, []string{})
|
||||
|
||||
ctx.Set(CookieAuthScopes, []string{})
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.WebhookRequestsList(ctx, webhook)
|
||||
return err
|
||||
}
|
||||
|
||||
// WorkerGet converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) WorkerGet(ctx echo.Context) error {
|
||||
var err error
|
||||
@@ -3510,6 +3555,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
|
||||
router.POST(baseURL+"/api/v1/users/register", wrapper.UserCreate)
|
||||
router.GET(baseURL+"/api/v1/users/slack/callback", wrapper.UserUpdateSlackOauthCallback)
|
||||
router.DELETE(baseURL+"/api/v1/webhook-workers/:webhook", wrapper.WebhookDelete)
|
||||
router.GET(baseURL+"/api/v1/webhook-workers/:webhook/requests", wrapper.WebhookRequestsList)
|
||||
router.GET(baseURL+"/api/v1/workers/:worker", wrapper.WorkerGet)
|
||||
router.PATCH(baseURL+"/api/v1/workers/:worker", wrapper.WorkerUpdate)
|
||||
router.GET(baseURL+"/api/v1/workflow-runs/:workflow-run/input", wrapper.WorkflowRunGetInput)
|
||||
@@ -5903,6 +5949,50 @@ func (response WebhookDelete405JSONResponse) VisitWebhookDeleteResponse(w http.R
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type WebhookRequestsListRequestObject struct {
|
||||
Webhook openapi_types.UUID `json:"webhook"`
|
||||
}
|
||||
|
||||
type WebhookRequestsListResponseObject interface {
|
||||
VisitWebhookRequestsListResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type WebhookRequestsList200JSONResponse WebhookWorkerRequestListResponse
|
||||
|
||||
func (response WebhookRequestsList200JSONResponse) VisitWebhookRequestsListResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type WebhookRequestsList400JSONResponse APIErrors
|
||||
|
||||
func (response WebhookRequestsList400JSONResponse) VisitWebhookRequestsListResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(400)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type WebhookRequestsList401JSONResponse APIErrors
|
||||
|
||||
func (response WebhookRequestsList401JSONResponse) VisitWebhookRequestsListResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(401)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type WebhookRequestsList405JSONResponse APIErrors
|
||||
|
||||
func (response WebhookRequestsList405JSONResponse) VisitWebhookRequestsListResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(405)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type WorkerGetRequestObject struct {
|
||||
Worker openapi_types.UUID `json:"worker"`
|
||||
Params WorkerGetParams
|
||||
@@ -6420,6 +6510,8 @@ type StrictServerInterface interface {
|
||||
|
||||
WebhookDelete(ctx echo.Context, request WebhookDeleteRequestObject) (WebhookDeleteResponseObject, error)
|
||||
|
||||
WebhookRequestsList(ctx echo.Context, request WebhookRequestsListRequestObject) (WebhookRequestsListResponseObject, error)
|
||||
|
||||
WorkerGet(ctx echo.Context, request WorkerGetRequestObject) (WorkerGetResponseObject, error)
|
||||
|
||||
WorkerUpdate(ctx echo.Context, request WorkerUpdateRequestObject) (WorkerUpdateResponseObject, error)
|
||||
@@ -8190,6 +8282,31 @@ func (sh *strictHandler) WebhookDelete(ctx echo.Context, webhook openapi_types.U
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebhookRequestsList operation middleware
|
||||
func (sh *strictHandler) WebhookRequestsList(ctx echo.Context, webhook openapi_types.UUID) error {
|
||||
var request WebhookRequestsListRequestObject
|
||||
|
||||
request.Webhook = webhook
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.WebhookRequestsList(ctx, request.(WebhookRequestsListRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "WebhookRequestsList")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(WebhookRequestsListResponseObject); ok {
|
||||
return validResponse.VisitWebhookRequestsListResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("Unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkerGet operation middleware
|
||||
func (sh *strictHandler) WorkerGet(ctx echo.Context, worker openapi_types.UUID, params WorkerGetParams) error {
|
||||
var request WorkerGetRequestObject
|
||||
@@ -8435,171 +8552,174 @@ func (sh *strictHandler) WorkflowVersionGetDefinition(ctx echo.Context, workflow
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@@ -6,6 +6,14 @@ import (
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/sqlchelpers"
|
||||
)
|
||||
|
||||
func ToWebhookWorkerRequest(webhookWorker *dbsqlc.WebhookWorkerRequest) *gen.WebhookWorkerRequest {
|
||||
return &gen.WebhookWorkerRequest{
|
||||
CreatedAt: webhookWorker.CreatedAt.Time,
|
||||
Method: webhookWorker.Method,
|
||||
StatusCode: int(webhookWorker.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
func ToWebhookWorker(webhookWorker *dbsqlc.WebhookWorker) *gen.WebhookWorker {
|
||||
return &gen.WebhookWorker{
|
||||
Metadata: *toAPIMetadata(
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/api/v1/server/oas/gen"
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/db"
|
||||
@@ -133,7 +134,7 @@ func ToWorker(worker *db.WorkerModel) *gen.Worker {
|
||||
return res
|
||||
}
|
||||
|
||||
func ToWorkerSqlc(worker *dbsqlc.Worker, slots *int, webhookUrl *string) *gen.Worker {
|
||||
func ToWorkerSqlc(worker *dbsqlc.Worker, slots *int, webhookUrl *string, actions []pgtype.Text) *gen.Worker {
|
||||
|
||||
dispatcherId := uuid.MustParse(pgUUIDToStr(worker.DispatcherId))
|
||||
|
||||
@@ -166,9 +167,24 @@ func ToWorkerSqlc(worker *dbsqlc.Worker, slots *int, webhookUrl *string) *gen.Wo
|
||||
WebhookUrl: webhookUrl,
|
||||
}
|
||||
|
||||
if worker.WebhookId.Valid {
|
||||
wid := uuid.MustParse(pgUUIDToStr(worker.WebhookId))
|
||||
res.WebhookId = &wid
|
||||
}
|
||||
|
||||
if !worker.LastHeartbeatAt.Time.IsZero() {
|
||||
res.LastHeartbeatAt = &worker.LastHeartbeatAt.Time
|
||||
}
|
||||
|
||||
if actions != nil {
|
||||
apiActions := make([]string, len(actions))
|
||||
|
||||
for i := range actions {
|
||||
apiActions[i] = actions[i].String
|
||||
}
|
||||
|
||||
res.Actions = &apiActions
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ const convert = new AnsiToHtml({
|
||||
bg: 'transparent',
|
||||
});
|
||||
|
||||
export interface ExtendedLogLine extends LogLine {
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
type LogProps = {
|
||||
logs: LogLine[];
|
||||
logs: ExtendedLogLine[];
|
||||
onTopReached: () => void;
|
||||
onBottomReached: () => void;
|
||||
};
|
||||
@@ -117,6 +121,10 @@ const LoggingComponent: React.FC<LogProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
const sortedLogs = [...showLogs].sort(
|
||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full mx-auto overflow-y-auto p-6 text-indigo-300 font-mono text-xs rounded-md max-h-[25rem] min-h-[25rem] bg-muted"
|
||||
@@ -128,7 +136,7 @@ const LoggingComponent: React.FC<LogProps> = ({
|
||||
Refreshing...
|
||||
</div>
|
||||
)}
|
||||
{showLogs.map((log, i) => {
|
||||
{sortedLogs.map((log, i) => {
|
||||
const sanitizedHtml = DOMPurify.sanitize(convert.toHtml(log.line), {
|
||||
USE_PROFILES: { html: true },
|
||||
});
|
||||
@@ -141,6 +149,7 @@ const LoggingComponent: React.FC<LogProps> = ({
|
||||
className="pb-2 break-all overflow-x-hidden"
|
||||
id={'log' + i}
|
||||
>
|
||||
{log.badge}
|
||||
<span className="text-gray-500 mr-2 ml--2">
|
||||
{new Date(log.timestamp)
|
||||
.toLocaleString('sv', options)
|
||||
|
||||
@@ -8,9 +8,15 @@ type Props = {
|
||||
text: string;
|
||||
className?: string;
|
||||
withText?: boolean;
|
||||
onCopy?: () => void;
|
||||
};
|
||||
|
||||
const CopyToClipboard: React.FC<Props> = ({ text, className, withText }) => {
|
||||
const CopyToClipboard: React.FC<Props> = ({
|
||||
text,
|
||||
className,
|
||||
withText,
|
||||
onCopy,
|
||||
}) => {
|
||||
const [successCopy, setSuccessCopy] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -25,7 +31,8 @@ const CopyToClipboard: React.FC<Props> = ({ text, className, withText }) => {
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setSuccessCopy(true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
onCopy && onCopy();
|
||||
setTimeout(() => {
|
||||
setSuccessCopy(false);
|
||||
}, 2000);
|
||||
|
||||
191
frontend/app/src/components/ui/secret-copier.tsx
Normal file
191
frontend/app/src/components/ui/secret-copier.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import typescript from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript';
|
||||
import yaml from 'react-syntax-highlighter/dist/esm/languages/hljs/yaml';
|
||||
import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
|
||||
import {
|
||||
anOldHope,
|
||||
atomOneLight,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import CopyToClipboard from './copy-to-clipboard';
|
||||
import { useRef, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '../theme-provider';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Button } from './button';
|
||||
import { CaretSortIcon } from '@radix-ui/react-icons';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
||||
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
||||
SyntaxHighlighter.registerLanguage('json', json);
|
||||
|
||||
type Secrets = Record<string, string>;
|
||||
|
||||
enum Formats {
|
||||
TABLE = 'table',
|
||||
JSON = 'json',
|
||||
YAML = 'yaml',
|
||||
DOTENV = 'dotenv',
|
||||
CLI = 'cli',
|
||||
}
|
||||
|
||||
export function SecretCopier({
|
||||
secrets,
|
||||
className,
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
copy,
|
||||
onClick,
|
||||
}: {
|
||||
secrets: Secrets;
|
||||
className?: string;
|
||||
maxHeight?: string;
|
||||
maxWidth?: string;
|
||||
copy?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [format, setFormat] = useState<Formats>(Formats.DOTENV);
|
||||
|
||||
const renderSecrets = () => {
|
||||
switch (format) {
|
||||
case Formats.JSON:
|
||||
return JSON.stringify(secrets, null, 2);
|
||||
case Formats.YAML:
|
||||
return toYAML(secrets);
|
||||
case Formats.TABLE:
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Env Var</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(secrets).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<CopyToClipboard text={key} /> {key}
|
||||
</td>
|
||||
<td>
|
||||
<CopyToClipboard text={value} /> {value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
case Formats.CLI:
|
||||
return toCliEnv(secrets);
|
||||
case Formats.DOTENV:
|
||||
default:
|
||||
return toDotEnv(secrets);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'w-full h-fit relative')}>
|
||||
<div className="mb-2 justify-right flex flex-row items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<span>{format}</span>
|
||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.DOTENV)}>
|
||||
{Formats.DOTENV}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.CLI)}>
|
||||
{Formats.CLI}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.JSON)}>
|
||||
{Formats.JSON}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.YAML)}>
|
||||
{Formats.YAML}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFormat(Formats.TABLE)}>
|
||||
{Formats.TABLE}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={() => textareaRef.current?.focus()}
|
||||
onClick={() => {
|
||||
textareaRef.current?.focus();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
onClick && onClick();
|
||||
}}
|
||||
className="relative flex bg-muted rounded-lg"
|
||||
>
|
||||
{format === Formats.TABLE ? (
|
||||
renderSecrets()
|
||||
) : (
|
||||
<SyntaxHighlighter
|
||||
language="text"
|
||||
style={theme === 'dark' ? anOldHope : atomOneLight}
|
||||
wrapLines={false}
|
||||
lineProps={{
|
||||
style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
|
||||
}}
|
||||
customStyle={{
|
||||
cursor: 'default',
|
||||
borderRadius: '0.5rem',
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
fontFamily:
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1rem',
|
||||
padding: '0.5rem',
|
||||
flex: '1',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
{renderSecrets() as string}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
</div>
|
||||
{copy && format !== Formats.TABLE && (
|
||||
<CopyToClipboard
|
||||
text={renderSecrets() as string}
|
||||
withText
|
||||
onCopy={() => onClick && onClick()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function toDotEnv(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function toCliEnv(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `export ${key}="${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function toYAML(s: Secrets) {
|
||||
return Object.entries(s)
|
||||
.map(([key, value]) => `${key}:"${value}"`)
|
||||
.join('\n');
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
WebhookWorkerCreated,
|
||||
WebhookWorkerCreateRequest,
|
||||
WebhookWorkerListResponse,
|
||||
WebhookWorkerRequestListResponse,
|
||||
Worker,
|
||||
WorkerList,
|
||||
Workflow,
|
||||
@@ -1641,6 +1642,22 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
secure: true,
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* @description Lists all requests for a webhook
|
||||
*
|
||||
* @name WebhookRequestsList
|
||||
* @summary List webhook requests
|
||||
* @request GET:/api/v1/webhook-workers/{webhook}/requests
|
||||
* @secure
|
||||
*/
|
||||
webhookRequestsList = (webhook: string, params: RequestParams = {}) =>
|
||||
this.request<WebhookWorkerRequestListResponse, APIErrors>({
|
||||
path: `/api/v1/webhook-workers/${webhook}/requests`,
|
||||
method: 'GET',
|
||||
secure: true,
|
||||
format: 'json',
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* @description Get the input for a workflow run.
|
||||
*
|
||||
|
||||
@@ -946,6 +946,11 @@ export interface Worker {
|
||||
labels?: WorkerLabel[];
|
||||
/** The webhook URL for the worker. */
|
||||
webhookUrl?: string;
|
||||
/**
|
||||
* The webhook ID for the worker.
|
||||
* @format uuid
|
||||
*/
|
||||
webhookId?: string;
|
||||
}
|
||||
|
||||
export interface WorkerLabel {
|
||||
@@ -1135,6 +1140,29 @@ export interface WebhookWorker {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export enum WebhookWorkerRequestMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
}
|
||||
|
||||
export interface WebhookWorkerRequest {
|
||||
/**
|
||||
* The date and time the request was created.
|
||||
* @format date-time
|
||||
*/
|
||||
created_at: string;
|
||||
/** The HTTP method used for the request. */
|
||||
method: WebhookWorkerRequestMethod;
|
||||
/** The HTTP status code of the response. */
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface WebhookWorkerRequestListResponse {
|
||||
/** The list of webhook requests. */
|
||||
requests?: WebhookWorkerRequest[];
|
||||
}
|
||||
|
||||
export interface WebhookWorkerCreated {
|
||||
metadata: APIResourceMeta;
|
||||
/** The name of the webhook worker. */
|
||||
|
||||
@@ -286,5 +286,10 @@ export const queries = createQueryKeyStore({
|
||||
queryFn: async () =>
|
||||
(await api.webhookCreate(tenant, webhookWorker)).data,
|
||||
}),
|
||||
listRequests: (webhookWorkerId: string) => ({
|
||||
queryKey: ['webhook-worker:list:requests', webhookWorkerId],
|
||||
queryFn: async () =>
|
||||
(await api.webhookRequestsList(webhookWorkerId)).data,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
import { CodeHighlighter } from '@/components/ui/code-highlighter';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { SecretCopier } from '@/components/ui/secret-copier';
|
||||
|
||||
export const EXPIRES_IN_OPTS = {
|
||||
'3 months': `${3 * 30 * 24 * 60 * 60}s`,
|
||||
@@ -65,12 +65,10 @@ export function CreateTokenDialog({
|
||||
This is the only time we will show you this token. Make sure to copy
|
||||
it somewhere safe.
|
||||
</p>
|
||||
<CodeHighlighter
|
||||
language="typescript"
|
||||
<SecretCopier
|
||||
secrets={{ HATCHET_CLIENT_TOKEN: token }}
|
||||
className="text-sm"
|
||||
wrapLines={false}
|
||||
maxWidth={'calc(700px - 4rem)'}
|
||||
code={'HATCHET_CLIENT_TOKEN="' + token + '"'}
|
||||
copy
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { DataTable } from '@/components/molecules/data-table/data-table';
|
||||
import { columns } from './components/step-runs-columns';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RecentWebhookRequests } from '../webhooks/components/recent-webhook-requests';
|
||||
export const isHealthy = (worker?: Worker) => {
|
||||
const reasons = [];
|
||||
|
||||
@@ -136,6 +137,7 @@ export default function ExpandedWorkflowRun() {
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row gap-4 items-center justify-between">
|
||||
<ServerStackIcon className="h-6 w-6 text-foreground mt-1" />
|
||||
<Badge>{worker.type}</Badge>
|
||||
<h2 className="text-2xl font-bold leading-tight text-foreground">
|
||||
<Link to="/workers">Workers/</Link>
|
||||
{worker.webhookUrl || worker.name}
|
||||
@@ -263,6 +265,18 @@ export default function ExpandedWorkflowRun() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{worker.webhookId && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
<div className="flex flex-row justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-bold leading-tight text-foreground">
|
||||
Recent HTTP Health Checks
|
||||
</h3>
|
||||
</div>
|
||||
<RecentWebhookRequests webhookId={worker.webhookId} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="my-4" />
|
||||
<h3 className="text-xl font-bold leading-tight text-foreground mb-4">
|
||||
Worker Labels
|
||||
|
||||
@@ -11,10 +11,12 @@ import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
import { CodeHighlighter } from '@/components/ui/code-highlighter';
|
||||
import { SecretCopier } from '@/components/ui/secret-copier';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RecentWebhookRequests } from './recent-webhook-requests';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
name: z.string().max(255).optional(),
|
||||
url: z.string().url().min(1).max(255),
|
||||
secret: z.string().min(1).max(255).optional(),
|
||||
});
|
||||
@@ -22,28 +24,59 @@ const schema = z.object({
|
||||
interface CreateWebhookWorkerDialogProps {
|
||||
className?: string;
|
||||
secret?: string;
|
||||
onSubmit: (opts: z.infer<typeof schema>) => void;
|
||||
webhookId?: string;
|
||||
onSubmit: (opts: z.infer<typeof schema> & { name: string }) => void;
|
||||
isLoading: boolean;
|
||||
fieldErrors?: Record<string, string>;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function CreateWebhookWorkerDialog({
|
||||
className,
|
||||
secret,
|
||||
webhookId,
|
||||
isOpen,
|
||||
...props
|
||||
}: CreateWebhookWorkerDialogProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<z.infer<typeof schema>>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
const [canTestConnection, setCanTestConnection] = useState(false);
|
||||
const [waitingForConnection, setWaitingForConnection] = useState(false);
|
||||
const [isComplete, setIsComplete] = useState(false);
|
||||
|
||||
const nameError = errors.name?.message?.toString() || props.fieldErrors?.name;
|
||||
const urlError = errors.url?.message?.toString() || props.fieldErrors?.url;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setCanTestConnection(false);
|
||||
setWaitingForConnection(false);
|
||||
setIsComplete(false);
|
||||
reset(); // Reset form fields
|
||||
}
|
||||
}, [isOpen, reset]);
|
||||
|
||||
if (isComplete) {
|
||||
return (
|
||||
<DialogContent className="w-fit max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connected!</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm">
|
||||
Your webhook worker is now connected and ready to receive runs.
|
||||
</p>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (secret) {
|
||||
return (
|
||||
<DialogContent className="w-fit max-w-[700px]">
|
||||
@@ -51,16 +84,40 @@ export function CreateWebhookWorkerDialog({
|
||||
<DialogTitle>Keep it secret, keep it safe</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm">
|
||||
Copy the webhook secret and add it in your application.
|
||||
Set the following Hatchet configuration in your application
|
||||
environment:
|
||||
</p>
|
||||
<CodeHighlighter
|
||||
language="typescript"
|
||||
<SecretCopier
|
||||
className="text-sm"
|
||||
wrapLines={false}
|
||||
maxWidth={'calc(700px - 4rem)'}
|
||||
code={secret}
|
||||
secrets={{
|
||||
HATCHET_WEBHOOK_SECRET: secret,
|
||||
}}
|
||||
copy
|
||||
onClick={() => setCanTestConnection(true)}
|
||||
/>
|
||||
|
||||
<p className="text-sm text-gray-500">
|
||||
These values should be kept secret and not shared with anyone. They
|
||||
will only be displayed once.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setWaitingForConnection(true);
|
||||
}}
|
||||
disabled={!canTestConnection}
|
||||
>
|
||||
{waitingForConnection && <Spinner />}
|
||||
Test Connection
|
||||
</Button>
|
||||
{waitingForConnection && webhookId && (
|
||||
<RecentWebhookRequests
|
||||
webhookId={webhookId}
|
||||
filterBeforeNow={true}
|
||||
onConnected={() => setIsComplete(true)}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
@@ -68,37 +125,26 @@ export function CreateWebhookWorkerDialog({
|
||||
return (
|
||||
<DialogContent className="w-fit max-w-[80%] min-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create a new Webhook Endpoint</DialogTitle>
|
||||
<DialogTitle>Create a New Webhook Worker</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className={cn('grid gap-6', className)}>
|
||||
<form
|
||||
onSubmit={handleSubmit((d) => {
|
||||
props.onSubmit(d);
|
||||
const name = d.name || d.url || '';
|
||||
props.onSubmit({ ...d, name });
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
{...register('name')}
|
||||
id="webhook-worker-name"
|
||||
name="name"
|
||||
placeholder="My Webhook Endpoint"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
disabled={props.isLoading}
|
||||
/>
|
||||
{nameError && (
|
||||
<div className="text-sm text-red-500">{nameError}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="url">URL</Label>
|
||||
<p className="text-sm dark:text-gray-400 text-gray-800">
|
||||
The URL with full path where the webhook worker will be
|
||||
available.
|
||||
</p>
|
||||
<Input
|
||||
{...register('url')}
|
||||
id="webhook-worker-url"
|
||||
name="url"
|
||||
placeholder="The Webhook URL"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
disabled={props.isLoading}
|
||||
@@ -107,10 +153,27 @@ export function CreateWebhookWorkerDialog({
|
||||
<div className="text-sm text-red-500">{urlError}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Friendly Name (optional)</Label>
|
||||
<p className="text-sm dark:text-gray-400 text-gray-800">
|
||||
An easy to remember name to identify worker.
|
||||
</p>
|
||||
<Input
|
||||
{...register('name')}
|
||||
id="webhook-worker-name"
|
||||
name="name"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
disabled={props.isLoading}
|
||||
/>
|
||||
{nameError && (
|
||||
<div className="text-sm text-red-500">{nameError}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button disabled={props.isLoading}>
|
||||
{props.isLoading && <Spinner />}
|
||||
Create
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import LoggingComponent, {
|
||||
ExtendedLogLine,
|
||||
} from '@/components/cloud/logging/logs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { queries } from '@/lib/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface RecentRequestProps {
|
||||
webhookId: string;
|
||||
onConnected?: () => void;
|
||||
filterBeforeNow?: boolean;
|
||||
}
|
||||
|
||||
const StatusCodeToMessage: Record<number, string> = {
|
||||
200: 'Server can receive run requests!',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden, Check if worker path is correct',
|
||||
404: 'Not Found, Check if worker path is correct',
|
||||
500: 'Internal Server Error, See server worker logs',
|
||||
502: 'Bad Gateway, Check if domain is correct and the server is running',
|
||||
};
|
||||
|
||||
export const RecentWebhookRequests: React.FC<RecentRequestProps> = ({
|
||||
webhookId,
|
||||
onConnected,
|
||||
filterBeforeNow = false,
|
||||
}) => {
|
||||
const [timeAfter] = useState(filterBeforeNow ? Date.now() : undefined);
|
||||
|
||||
const webhookRequestQuery = useQuery({
|
||||
...queries.webhookWorkers.listRequests(webhookId),
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const filteredRequests = timeAfter
|
||||
? webhookRequestQuery.data?.requests?.filter(
|
||||
(request) => new Date(request.created_at).getTime() > timeAfter,
|
||||
)
|
||||
: webhookRequestQuery.data?.requests;
|
||||
|
||||
useEffect(() => {
|
||||
if (!onConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filteredRequests || filteredRequests.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filteredRequests[0].statusCode === 200) {
|
||||
onConnected();
|
||||
}
|
||||
}, [onConnected, filteredRequests]);
|
||||
|
||||
const logLines = useMemo(() => {
|
||||
return (filteredRequests || []).map<ExtendedLogLine>((request) => {
|
||||
return {
|
||||
line: StatusCodeToMessage[request.statusCode],
|
||||
timestamp: request.created_at,
|
||||
instance: '',
|
||||
badge: (
|
||||
<Badge
|
||||
className="mr-4"
|
||||
variant={request.statusCode == 200 ? 'successful' : 'failed'}
|
||||
>
|
||||
{request.statusCode}
|
||||
</Badge>
|
||||
),
|
||||
|
||||
// statusCode: request.statusCode,
|
||||
// createdAt: request.created_at,
|
||||
// message: StatusCodeToMessage[request.statusCode],
|
||||
// metadata: {},
|
||||
};
|
||||
});
|
||||
}, [filteredRequests]);
|
||||
|
||||
if (webhookRequestQuery.isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (webhookRequestQuery.isError) {
|
||||
return <div>Error: {webhookRequestQuery.error?.message}</div>;
|
||||
}
|
||||
|
||||
if (
|
||||
!webhookRequestQuery.data ||
|
||||
!webhookRequestQuery.data.requests ||
|
||||
webhookRequestQuery.data.requests.length === 0
|
||||
) {
|
||||
return <div>Attempting to connect...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<LoggingComponent
|
||||
logs={logLines}
|
||||
onTopReached={function (): void {}}
|
||||
onBottomReached={function (): void {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card.tsx';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApiError } from '@/lib/hooks.ts';
|
||||
import { Dialog } from '@/components/ui/dialog.tsx';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { BiDotsVertical } from 'react-icons/bi';
|
||||
import { CreateWebhookWorkerDialog } from './components/create-webhook-worker-dialog';
|
||||
import { DeleteWebhookWorkerDialog } from './components/delete-webhook-worker-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export default function Webhooks() {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
@@ -36,8 +37,8 @@ export default function Webhooks() {
|
||||
<div className="flex-grow h-full w-full">
|
||||
<div className="mx-auto max-w-7xl py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<h2 className="text-2xl font-semibold leading-tight text-foreground">
|
||||
Webhook Workers (BETA)
|
||||
<h2 className="text-2xl font-semibold leading-tight text-foreground flex flex-row items-center gap-2">
|
||||
Webhook Workers <Badge variant="inProgress">BETA</Badge>
|
||||
</h2>
|
||||
|
||||
<Button
|
||||
@@ -48,7 +49,15 @@ export default function Webhooks() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-gray-700 dark:text-gray-300 my-4">
|
||||
Assign webhook workers to workflows.
|
||||
Assign workflow runs to a HTTP endpoint.{' '}
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
href="https://docs.hatchet.run/home/features/webhooks"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</p>
|
||||
<Separator className="my-4" />
|
||||
|
||||
@@ -174,6 +183,7 @@ function CreateWebhookWorker({
|
||||
setShowDialog: (show: boolean) => void;
|
||||
}) {
|
||||
const [generatedToken, setGeneratedToken] = useState<string | undefined>();
|
||||
const [webhookId, setWebhookId] = useState<string | undefined>();
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
const { handleApiError } = useApiError({
|
||||
setFieldErrors: setFieldErrors,
|
||||
@@ -187,18 +197,29 @@ function CreateWebhookWorker({
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setGeneratedToken(data.secret);
|
||||
setWebhookId(data.metadata.id);
|
||||
onSuccess();
|
||||
},
|
||||
onError: handleApiError,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (showDialog) {
|
||||
setGeneratedToken(undefined);
|
||||
setWebhookId(undefined);
|
||||
setFieldErrors({});
|
||||
}
|
||||
}, [showDialog]);
|
||||
|
||||
return (
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<CreateWebhookWorkerDialog
|
||||
isLoading={createWebhookWorkerMutation.isPending}
|
||||
onSubmit={createWebhookWorkerMutation.mutate}
|
||||
secret={generatedToken}
|
||||
webhookId={webhookId}
|
||||
fieldErrors={fieldErrors}
|
||||
isOpen={showDialog}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -154,7 +154,7 @@ func (c *WebhooksController) cleanupDeletedWorker(id, tenantId string) {
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
})
|
||||
if err != nil {
|
||||
c.sc.Logger.Err(err).Msgf("could not delete webhook worker")
|
||||
c.sc.Logger.Err(err).Msgf("could not delete webhook worker worker")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,6 +162,12 @@ func (c *WebhooksController) cleanupDeletedWorker(id, tenantId string) {
|
||||
delete(c.registeredWorkerIds, id)
|
||||
delete(c.cleanups, id)
|
||||
c.mu.Unlock()
|
||||
|
||||
err = c.sc.EngineRepository.WebhookWorker().HardDeleteWebhookWorker(context.Background(), id, tenantId)
|
||||
|
||||
if err != nil {
|
||||
c.sc.Logger.Err(err).Msgf("could not delete webhook worker")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WebhooksController) getOrCreateToken(ww *dbsqlc.WebhookWorker, tenantId string) (string, error) {
|
||||
@@ -190,14 +196,15 @@ func (c *WebhooksController) getOrCreateToken(ww *dbsqlc.WebhookWorker, tenantId
|
||||
|
||||
encTokStr := base64.StdEncoding.EncodeToString(encTok)
|
||||
|
||||
_, err = c.sc.EngineRepository.WebhookWorker().UpsertWebhookWorker(context.Background(), &repository.UpsertWebhookWorkerOpts{
|
||||
Name: ww.Name,
|
||||
URL: ww.Url,
|
||||
Secret: ww.Secret,
|
||||
TenantId: tenantId,
|
||||
TokenID: &tok.TokenId,
|
||||
TokenValue: &encTokStr,
|
||||
})
|
||||
_, err = c.sc.EngineRepository.WebhookWorker().UpdateWebhookWorkerToken(
|
||||
context.Background(),
|
||||
sqlchelpers.UUIDToStr(ww.ID),
|
||||
tenantId,
|
||||
&repository.UpdateWebhookWorkerTokenOpts{
|
||||
TokenID: &tok.TokenId,
|
||||
TokenValue: &encTokStr,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not update webhook worker: %w", err)
|
||||
}
|
||||
@@ -215,15 +222,21 @@ func (c *WebhooksController) healthcheck(ww *dbsqlc.WebhookWorker) (*HealthCheck
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := whrequest.Send(context.Background(), ww.Url, secret, struct {
|
||||
resp, statusCode, err := whrequest.Send(context.Background(), ww.Url, secret, struct {
|
||||
Time time.Time `json:"time"`
|
||||
}{
|
||||
Time: time.Now(),
|
||||
}, func(req *http.Request) {
|
||||
req.Method = "PUT"
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("healthcheck request: %w", err)
|
||||
|
||||
if statusCode != nil {
|
||||
err = c.sc.EngineRepository.WebhookWorker().InsertWebhookWorkerRequest(context.Background(), sqlchelpers.UUIDToStr(ww.ID), "PUT", int32(*statusCode))
|
||||
c.sc.Logger.Err(err).Msgf("could not insert webhook worker request")
|
||||
}
|
||||
|
||||
if err != nil || *statusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("health check request: %w", err)
|
||||
}
|
||||
|
||||
var res HealthCheckResponse
|
||||
|
||||
@@ -12,20 +12,20 @@ import (
|
||||
"github.com/hatchet-dev/hatchet/internal/signature"
|
||||
)
|
||||
|
||||
func Send(ctx context.Context, url string, secret string, data any, headers ...func(req *http.Request)) ([]byte, error) {
|
||||
func Send(ctx context.Context, url string, secret string, data any, headers ...func(req *http.Request)) ([]byte, *int, error) {
|
||||
body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sig, err := signature.Sign(string(body), secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
req.Header.Set("X-Hatchet-Signature", sig)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -39,22 +39,25 @@ func Send(ctx context.Context, url string, secret string, data any, headers ...f
|
||||
Timeout: time.Second * 600,
|
||||
}
|
||||
|
||||
// TODO block-list
|
||||
|
||||
// nolint:gosec
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
connRefused := 502
|
||||
return nil, &connRefused, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed with status code %d", resp.StatusCode)
|
||||
return nil, &resp.StatusCode, fmt.Errorf("request failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
res, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read response body: %w", err)
|
||||
return nil, &resp.StatusCode, fmt.Errorf("could not read response body: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
return res, &resp.StatusCode, nil
|
||||
}
|
||||
|
||||
@@ -963,6 +963,25 @@ type WebhookWorkerListResponse struct {
|
||||
Rows *[]WebhookWorker `json:"rows,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequest defines model for WebhookWorkerRequest.
|
||||
type WebhookWorkerRequest struct {
|
||||
// CreatedAt The date and time the request was created.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Method WebhookWorkerRequestMethod `json:"method"`
|
||||
|
||||
// StatusCode The HTTP status code of the response.
|
||||
StatusCode int `json:"statusCode"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequestListResponse defines model for WebhookWorkerRequestListResponse.
|
||||
type WebhookWorkerRequestListResponse struct {
|
||||
// Requests The list of webhook requests.
|
||||
Requests *[]WebhookWorkerRequest `json:"requests,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookWorkerRequestMethod defines model for WebhookWorkerRequestMethod.
|
||||
type WebhookWorkerRequestMethod = interface{}
|
||||
|
||||
// Worker defines model for Worker.
|
||||
type Worker struct {
|
||||
// Actions The actions this worker can perform.
|
||||
@@ -1000,6 +1019,9 @@ type Worker struct {
|
||||
Status *WorkerStatus `json:"status,omitempty"`
|
||||
Type WorkerType `json:"type"`
|
||||
|
||||
// WebhookId The webhook ID for the worker.
|
||||
WebhookId *openapi_types.UUID `json:"webhookId,omitempty"`
|
||||
|
||||
// WebhookUrl The webhook URL for the worker.
|
||||
WebhookUrl *string `json:"webhookUrl,omitempty"`
|
||||
}
|
||||
@@ -1761,6 +1783,9 @@ type ClientInterface interface {
|
||||
// WebhookDelete request
|
||||
WebhookDelete(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// WebhookRequestsList request
|
||||
WebhookRequestsList(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// WorkerGet request
|
||||
WorkerGet(ctx context.Context, worker openapi_types.UUID, params *WorkerGetParams, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -2813,6 +2838,18 @@ func (c *Client) WebhookDelete(ctx context.Context, webhook openapi_types.UUID,
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) WebhookRequestsList(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewWebhookRequestsListRequest(c.Server, webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) WorkerGet(ctx context.Context, worker openapi_types.UUID, params *WorkerGetParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewWorkerGetRequest(c.Server, worker, params)
|
||||
if err != nil {
|
||||
@@ -6057,6 +6094,40 @@ func NewWebhookDeleteRequest(server string, webhook openapi_types.UUID) (*http.R
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewWebhookRequestsListRequest generates requests for WebhookRequestsList
|
||||
func NewWebhookRequestsListRequest(server string, webhook openapi_types.UUID) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithLocation("simple", false, "webhook", runtime.ParamLocationPath, webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/api/v1/webhook-workers/%s/requests", pathParam0)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewWorkerGetRequest generates requests for WorkerGet
|
||||
func NewWorkerGetRequest(server string, worker openapi_types.UUID, params *WorkerGetParams) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -6794,6 +6865,9 @@ type ClientWithResponsesInterface interface {
|
||||
// WebhookDeleteWithResponse request
|
||||
WebhookDeleteWithResponse(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*WebhookDeleteResponse, error)
|
||||
|
||||
// WebhookRequestsListWithResponse request
|
||||
WebhookRequestsListWithResponse(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*WebhookRequestsListResponse, error)
|
||||
|
||||
// WorkerGetWithResponse request
|
||||
WorkerGetWithResponse(ctx context.Context, worker openapi_types.UUID, params *WorkerGetParams, reqEditors ...RequestEditorFn) (*WorkerGetResponse, error)
|
||||
|
||||
@@ -8398,6 +8472,31 @@ func (r WebhookDeleteResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type WebhookRequestsListResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *WebhookWorkerRequestListResponse
|
||||
JSON400 *APIErrors
|
||||
JSON401 *APIErrors
|
||||
JSON405 *APIErrors
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r WebhookRequestsListResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r WebhookRequestsListResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type WorkerGetResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -9366,6 +9465,15 @@ func (c *ClientWithResponses) WebhookDeleteWithResponse(ctx context.Context, web
|
||||
return ParseWebhookDeleteResponse(rsp)
|
||||
}
|
||||
|
||||
// WebhookRequestsListWithResponse request returning *WebhookRequestsListResponse
|
||||
func (c *ClientWithResponses) WebhookRequestsListWithResponse(ctx context.Context, webhook openapi_types.UUID, reqEditors ...RequestEditorFn) (*WebhookRequestsListResponse, error) {
|
||||
rsp, err := c.WebhookRequestsList(ctx, webhook, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseWebhookRequestsListResponse(rsp)
|
||||
}
|
||||
|
||||
// WorkerGetWithResponse request returning *WorkerGetResponse
|
||||
func (c *ClientWithResponses) WorkerGetWithResponse(ctx context.Context, worker openapi_types.UUID, params *WorkerGetParams, reqEditors ...RequestEditorFn) (*WorkerGetResponse, error) {
|
||||
rsp, err := c.WorkerGet(ctx, worker, params, reqEditors...)
|
||||
@@ -11983,6 +12091,53 @@ func ParseWebhookDeleteResponse(rsp *http.Response) (*WebhookDeleteResponse, err
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseWebhookRequestsListResponse parses an HTTP response from a WebhookRequestsListWithResponse call
|
||||
func ParseWebhookRequestsListResponse(rsp *http.Response) (*WebhookRequestsListResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &WebhookRequestsListResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest WebhookWorkerRequestListResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
|
||||
var dest APIErrors
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON400 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
|
||||
var dest APIErrors
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON401 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 405:
|
||||
var dest APIErrors
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON405 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseWorkerGetResponse parses an HTTP response from a WorkerGetWithResponse call
|
||||
func ParseWorkerGetResponse(rsp *http.Response) (*WorkerGetResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -588,6 +588,49 @@ func (ns NullVcsProvider) Value() (driver.Value, error) {
|
||||
return string(ns.VcsProvider), nil
|
||||
}
|
||||
|
||||
type WebhookWorkerRequestMethod string
|
||||
|
||||
const (
|
||||
WebhookWorkerRequestMethodGET WebhookWorkerRequestMethod = "GET"
|
||||
WebhookWorkerRequestMethodPOST WebhookWorkerRequestMethod = "POST"
|
||||
WebhookWorkerRequestMethodPUT WebhookWorkerRequestMethod = "PUT"
|
||||
)
|
||||
|
||||
func (e *WebhookWorkerRequestMethod) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = WebhookWorkerRequestMethod(s)
|
||||
case string:
|
||||
*e = WebhookWorkerRequestMethod(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for WebhookWorkerRequestMethod: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullWebhookWorkerRequestMethod struct {
|
||||
WebhookWorkerRequestMethod WebhookWorkerRequestMethod `json:"WebhookWorkerRequestMethod"`
|
||||
Valid bool `json:"valid"` // Valid is true if WebhookWorkerRequestMethod is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullWebhookWorkerRequestMethod) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.WebhookWorkerRequestMethod, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.WebhookWorkerRequestMethod.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullWebhookWorkerRequestMethod) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.WebhookWorkerRequestMethod), nil
|
||||
}
|
||||
|
||||
type WorkerLabelComparator string
|
||||
|
||||
const (
|
||||
@@ -1235,6 +1278,14 @@ type WebhookWorker struct {
|
||||
TenantId pgtype.UUID `json:"tenantId"`
|
||||
}
|
||||
|
||||
type WebhookWorkerRequest struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
CreatedAt pgtype.Timestamp `json:"createdAt"`
|
||||
WebhookWorkerId pgtype.UUID `json:"webhookWorkerId"`
|
||||
Method WebhookWorkerRequestMethod `json:"method"`
|
||||
StatusCode int32 `json:"statusCode"`
|
||||
}
|
||||
|
||||
type WebhookWorkerWorkflow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WebhookWorkerId pgtype.UUID `json:"webhookWorkerId"`
|
||||
|
||||
@@ -37,6 +37,9 @@ CREATE TYPE "TenantResourceLimitAlertType" AS ENUM ('Alarm', 'Exhausted');
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VcsProvider" AS ENUM ('GITHUB');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebhookWorkerRequestMethod" AS ENUM ('GET', 'POST', 'PUT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkerLabelComparator" AS ENUM ('EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'LESS_THAN', 'LESS_THAN_OR_EQUAL');
|
||||
|
||||
@@ -608,6 +611,17 @@ CREATE TABLE "WebhookWorker" (
|
||||
CONSTRAINT "WebhookWorker_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookWorkerRequest" (
|
||||
"id" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"webhookWorkerId" UUID NOT NULL,
|
||||
"method" "WebhookWorkerRequestMethod" NOT NULL,
|
||||
"statusCode" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "WebhookWorkerRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookWorkerWorkflow" (
|
||||
"id" UUID NOT NULL,
|
||||
@@ -1098,6 +1112,9 @@ CREATE UNIQUE INDEX "WebhookWorker_id_key" ON "WebhookWorker"("id" ASC);
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorker_url_key" ON "WebhookWorker"("url" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorkerRequest_id_key" ON "WebhookWorkerRequest"("id" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorkerWorkflow_id_key" ON "WebhookWorkerWorkflow"("id" ASC);
|
||||
|
||||
@@ -1428,6 +1445,9 @@ ALTER TABLE "WebhookWorker" ADD CONSTRAINT "WebhookWorker_tenantId_fkey" FOREIGN
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorker" ADD CONSTRAINT "WebhookWorker_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES "APIToken"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorkerRequest" ADD CONSTRAINT "WebhookWorkerRequest_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorkerWorkflow" ADD CONSTRAINT "WebhookWorkerWorkflow_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
@@ -24,8 +24,46 @@ SELECT *
|
||||
FROM "WebhookWorker"
|
||||
WHERE "tenantId" = @tenantId::uuid AND "deleted" = false;
|
||||
|
||||
-- name: ListWebhookWorkerRequests :many
|
||||
SELECT *
|
||||
FROM "WebhookWorkerRequest"
|
||||
WHERE "webhookWorkerId" = @webhookWorkerId::uuid
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT 50;
|
||||
|
||||
-- name: UpsertWebhookWorker :one
|
||||
-- name: InsertWebhookWorkerRequest :exec
|
||||
WITH delete_old AS (
|
||||
-- Delete old requests
|
||||
DELETE FROM "WebhookWorkerRequest"
|
||||
WHERE "webhookWorkerId" = @webhookWorkerId::uuid
|
||||
AND "createdAt" < NOW() - INTERVAL '15 minutes'
|
||||
)
|
||||
INSERT INTO "WebhookWorkerRequest" (
|
||||
"id",
|
||||
"createdAt",
|
||||
"webhookWorkerId",
|
||||
"method",
|
||||
"statusCode"
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
CURRENT_TIMESTAMP,
|
||||
@webhookWorkerId::uuid,
|
||||
@method::"WebhookWorkerRequestMethod",
|
||||
@statusCode::integer
|
||||
);
|
||||
|
||||
-- name: UpdateWebhookWorkerToken :one
|
||||
UPDATE "WebhookWorker"
|
||||
SET
|
||||
"updatedAt" = CURRENT_TIMESTAMP,
|
||||
"tokenValue" = COALESCE(sqlc.narg('tokenValue')::text, "tokenValue"),
|
||||
"tokenId" = COALESCE(sqlc.narg('tokenId')::uuid, "tokenId")
|
||||
WHERE
|
||||
"id" = @id::uuid
|
||||
AND "tenantId" = @tenantId::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateWebhookWorker :one
|
||||
INSERT INTO "WebhookWorker" (
|
||||
"id",
|
||||
"createdAt",
|
||||
@@ -50,20 +88,19 @@ VALUES (
|
||||
sqlc.narg('tokenValue')::text,
|
||||
coalesce(sqlc.narg('deleted')::boolean, false)
|
||||
)
|
||||
ON CONFLICT ("url") DO
|
||||
UPDATE
|
||||
SET
|
||||
"tokenId" = coalesce(sqlc.narg('tokenId')::uuid, excluded."tokenId"),
|
||||
"tokenValue" = coalesce(sqlc.narg('tokenValue')::text, excluded."tokenValue"),
|
||||
"name" = coalesce(sqlc.narg('name')::text, excluded."name"),
|
||||
"secret" = coalesce(sqlc.narg('secret')::text, excluded."secret"),
|
||||
"url" = coalesce(sqlc.narg('url')::text, excluded."url"),
|
||||
"deleted" = coalesce(sqlc.narg('deleted')::boolean, excluded."deleted")
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteWebhookWorker :exec
|
||||
-- name: SoftDeleteWebhookWorker :exec
|
||||
UPDATE "WebhookWorker"
|
||||
SET "deleted" = true
|
||||
SET
|
||||
"deleted" = true,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
"id" = @id::uuid
|
||||
and "tenantId" = @tenantId::uuid;
|
||||
AND "tenantId" = @tenantId::uuid;
|
||||
|
||||
-- name: HardDeleteWebhookWorker :exec
|
||||
DELETE FROM "WebhookWorker"
|
||||
WHERE
|
||||
"id" = @id::uuid
|
||||
AND "tenantId" = @tenantId::uuid;
|
||||
|
||||
@@ -11,21 +11,117 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const deleteWebhookWorker = `-- name: DeleteWebhookWorker :exec
|
||||
UPDATE "WebhookWorker"
|
||||
SET "deleted" = true
|
||||
WHERE
|
||||
"id" = $1::uuid
|
||||
and "tenantId" = $2::uuid
|
||||
const createWebhookWorker = `-- name: CreateWebhookWorker :one
|
||||
INSERT INTO "WebhookWorker" (
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"name",
|
||||
"secret",
|
||||
"url",
|
||||
"tenantId",
|
||||
"tokenId",
|
||||
"tokenValue",
|
||||
"deleted"
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP,
|
||||
$1::text,
|
||||
$2::text,
|
||||
$3::text,
|
||||
$4::uuid,
|
||||
$5::uuid,
|
||||
$6::text,
|
||||
coalesce($7::boolean, false)
|
||||
)
|
||||
RETURNING id, "createdAt", "updatedAt", name, secret, url, "tokenValue", deleted, "tokenId", "tenantId"
|
||||
`
|
||||
|
||||
type DeleteWebhookWorkerParams struct {
|
||||
type CreateWebhookWorkerParams struct {
|
||||
Name string `json:"name"`
|
||||
Secret string `json:"secret"`
|
||||
Url string `json:"url"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
TokenId pgtype.UUID `json:"tokenId"`
|
||||
TokenValue pgtype.Text `json:"tokenValue"`
|
||||
Deleted pgtype.Bool `json:"deleted"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateWebhookWorker(ctx context.Context, db DBTX, arg CreateWebhookWorkerParams) (*WebhookWorker, error) {
|
||||
row := db.QueryRow(ctx, createWebhookWorker,
|
||||
arg.Name,
|
||||
arg.Secret,
|
||||
arg.Url,
|
||||
arg.Tenantid,
|
||||
arg.TokenId,
|
||||
arg.TokenValue,
|
||||
arg.Deleted,
|
||||
)
|
||||
var i WebhookWorker
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Secret,
|
||||
&i.Url,
|
||||
&i.TokenValue,
|
||||
&i.Deleted,
|
||||
&i.TokenId,
|
||||
&i.TenantId,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const hardDeleteWebhookWorker = `-- name: HardDeleteWebhookWorker :exec
|
||||
DELETE FROM "WebhookWorker"
|
||||
WHERE
|
||||
"id" = $1::uuid
|
||||
AND "tenantId" = $2::uuid
|
||||
`
|
||||
|
||||
type HardDeleteWebhookWorkerParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteWebhookWorker(ctx context.Context, db DBTX, arg DeleteWebhookWorkerParams) error {
|
||||
_, err := db.Exec(ctx, deleteWebhookWorker, arg.ID, arg.Tenantid)
|
||||
func (q *Queries) HardDeleteWebhookWorker(ctx context.Context, db DBTX, arg HardDeleteWebhookWorkerParams) error {
|
||||
_, err := db.Exec(ctx, hardDeleteWebhookWorker, arg.ID, arg.Tenantid)
|
||||
return err
|
||||
}
|
||||
|
||||
const insertWebhookWorkerRequest = `-- name: InsertWebhookWorkerRequest :exec
|
||||
WITH delete_old AS (
|
||||
-- Delete old requests
|
||||
DELETE FROM "WebhookWorkerRequest"
|
||||
WHERE "webhookWorkerId" = $1::uuid
|
||||
AND "createdAt" < NOW() - INTERVAL '15 minutes'
|
||||
)
|
||||
INSERT INTO "WebhookWorkerRequest" (
|
||||
"id",
|
||||
"createdAt",
|
||||
"webhookWorkerId",
|
||||
"method",
|
||||
"statusCode"
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
CURRENT_TIMESTAMP,
|
||||
$1::uuid,
|
||||
$2::"WebhookWorkerRequestMethod",
|
||||
$3::integer
|
||||
)
|
||||
`
|
||||
|
||||
type InsertWebhookWorkerRequestParams struct {
|
||||
Webhookworkerid pgtype.UUID `json:"webhookworkerid"`
|
||||
Method WebhookWorkerRequestMethod `json:"method"`
|
||||
Statuscode int32 `json:"statuscode"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertWebhookWorkerRequest(ctx context.Context, db DBTX, arg InsertWebhookWorkerRequestParams) error {
|
||||
_, err := db.Exec(ctx, insertWebhookWorkerRequest, arg.Webhookworkerid, arg.Method, arg.Statuscode)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -66,6 +162,40 @@ func (q *Queries) ListActiveWebhookWorkers(ctx context.Context, db DBTX, tenanti
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listWebhookWorkerRequests = `-- name: ListWebhookWorkerRequests :many
|
||||
SELECT id, "createdAt", "webhookWorkerId", method, "statusCode"
|
||||
FROM "WebhookWorkerRequest"
|
||||
WHERE "webhookWorkerId" = $1::uuid
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT 50
|
||||
`
|
||||
|
||||
func (q *Queries) ListWebhookWorkerRequests(ctx context.Context, db DBTX, webhookworkerid pgtype.UUID) ([]*WebhookWorkerRequest, error) {
|
||||
rows, err := db.Query(ctx, listWebhookWorkerRequests, webhookworkerid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []*WebhookWorkerRequest
|
||||
for rows.Next() {
|
||||
var i WebhookWorkerRequest
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.WebhookWorkerId,
|
||||
&i.Method,
|
||||
&i.StatusCode,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, &i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listWebhookWorkersByPartitionId = `-- name: ListWebhookWorkersByPartitionId :many
|
||||
WITH tenants AS (
|
||||
SELECT
|
||||
@@ -119,62 +249,51 @@ func (q *Queries) ListWebhookWorkersByPartitionId(ctx context.Context, db DBTX,
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertWebhookWorker = `-- name: UpsertWebhookWorker :one
|
||||
INSERT INTO "WebhookWorker" (
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"name",
|
||||
"secret",
|
||||
"url",
|
||||
"tenantId",
|
||||
"tokenId",
|
||||
"tokenValue",
|
||||
"deleted"
|
||||
)
|
||||
VALUES (
|
||||
gen_random_uuid(),
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP,
|
||||
$1::text,
|
||||
$2::text,
|
||||
$3::text,
|
||||
$4::uuid,
|
||||
$5::uuid,
|
||||
$6::text,
|
||||
coalesce($7::boolean, false)
|
||||
)
|
||||
ON CONFLICT ("url") DO
|
||||
UPDATE
|
||||
const softDeleteWebhookWorker = `-- name: SoftDeleteWebhookWorker :exec
|
||||
UPDATE "WebhookWorker"
|
||||
SET
|
||||
"tokenId" = coalesce($5::uuid, excluded."tokenId"),
|
||||
"tokenValue" = coalesce($6::text, excluded."tokenValue"),
|
||||
"name" = coalesce($1::text, excluded."name"),
|
||||
"secret" = coalesce($2::text, excluded."secret"),
|
||||
"url" = coalesce($3::text, excluded."url"),
|
||||
"deleted" = coalesce($7::boolean, excluded."deleted")
|
||||
"deleted" = true,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
"id" = $1::uuid
|
||||
AND "tenantId" = $2::uuid
|
||||
`
|
||||
|
||||
type SoftDeleteWebhookWorkerParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
}
|
||||
|
||||
func (q *Queries) SoftDeleteWebhookWorker(ctx context.Context, db DBTX, arg SoftDeleteWebhookWorkerParams) error {
|
||||
_, err := db.Exec(ctx, softDeleteWebhookWorker, arg.ID, arg.Tenantid)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWebhookWorkerToken = `-- name: UpdateWebhookWorkerToken :one
|
||||
UPDATE "WebhookWorker"
|
||||
SET
|
||||
"updatedAt" = CURRENT_TIMESTAMP,
|
||||
"tokenValue" = COALESCE($1::text, "tokenValue"),
|
||||
"tokenId" = COALESCE($2::uuid, "tokenId")
|
||||
WHERE
|
||||
"id" = $3::uuid
|
||||
AND "tenantId" = $4::uuid
|
||||
RETURNING id, "createdAt", "updatedAt", name, secret, url, "tokenValue", deleted, "tokenId", "tenantId"
|
||||
`
|
||||
|
||||
type UpsertWebhookWorkerParams struct {
|
||||
Name pgtype.Text `json:"name"`
|
||||
Secret pgtype.Text `json:"secret"`
|
||||
Url pgtype.Text `json:"url"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
TokenId pgtype.UUID `json:"tokenId"`
|
||||
type UpdateWebhookWorkerTokenParams struct {
|
||||
TokenValue pgtype.Text `json:"tokenValue"`
|
||||
Deleted pgtype.Bool `json:"deleted"`
|
||||
TokenId pgtype.UUID `json:"tokenId"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertWebhookWorker(ctx context.Context, db DBTX, arg UpsertWebhookWorkerParams) (*WebhookWorker, error) {
|
||||
row := db.QueryRow(ctx, upsertWebhookWorker,
|
||||
arg.Name,
|
||||
arg.Secret,
|
||||
arg.Url,
|
||||
arg.Tenantid,
|
||||
arg.TokenId,
|
||||
func (q *Queries) UpdateWebhookWorkerToken(ctx context.Context, db DBTX, arg UpdateWebhookWorkerTokenParams) (*WebhookWorker, error) {
|
||||
row := db.QueryRow(ctx, updateWebhookWorkerToken,
|
||||
arg.TokenValue,
|
||||
arg.Deleted,
|
||||
arg.TokenId,
|
||||
arg.ID,
|
||||
arg.Tenantid,
|
||||
)
|
||||
var i WebhookWorker
|
||||
err := row.Scan(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
SELECT
|
||||
sqlc.embed(workers),
|
||||
ww."url" AS "webhookUrl",
|
||||
ww."id" AS "webhookId",
|
||||
(SELECT COUNT(*) FROM "WorkerSemaphoreSlot" wss WHERE wss."workerId" = workers."id" AND wss."stepRunId" IS NOT NULL) AS "slots"
|
||||
FROM
|
||||
"Worker" workers
|
||||
@@ -32,18 +33,34 @@ WHERE
|
||||
))
|
||||
)
|
||||
GROUP BY
|
||||
workers."id", ww."url";
|
||||
workers."id", ww."url", ww."id";
|
||||
|
||||
-- name: GetWorkerById :one
|
||||
SELECT
|
||||
sqlc.embed(workers),
|
||||
ww."url" AS "webhookUrl"
|
||||
sqlc.embed(w),
|
||||
ww."url" AS "webhookUrl",
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM "WorkerSemaphoreSlot"
|
||||
WHERE "workerId" = w.id AND "stepRunId" IS NOT NULL
|
||||
) AS filled_slots
|
||||
FROM
|
||||
"Worker" workers
|
||||
"Worker" w
|
||||
LEFT JOIN
|
||||
"WebhookWorker" ww ON workers."webhookId" = ww."id"
|
||||
"WebhookWorker" ww ON w."webhookId" = ww."id"
|
||||
WHERE
|
||||
workers."id" = @id::uuid;
|
||||
w."id" = @id::uuid;
|
||||
|
||||
-- name: GetWorkerActionsByWorkerId :many
|
||||
SELECT
|
||||
a."actionId" AS actionId
|
||||
FROM "Worker" w
|
||||
LEFT JOIN "_ActionToWorker" aw ON w.id = aw."B"
|
||||
LEFT JOIN "Action" a ON aw."A" = a.id
|
||||
WHERE
|
||||
a."tenantId" = @tenantId::uuid AND
|
||||
w."id" = @workerId::uuid;
|
||||
|
||||
|
||||
-- name: StubWorkerSemaphoreSlots :exec
|
||||
INSERT INTO "WorkerSemaphoreSlot" ("id", "workerId")
|
||||
|
||||
@@ -103,21 +103,63 @@ func (q *Queries) DeleteWorker(ctx context.Context, db DBTX, id pgtype.UUID) (*W
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getWorkerActionsByWorkerId = `-- name: GetWorkerActionsByWorkerId :many
|
||||
SELECT
|
||||
a."actionId" AS actionId
|
||||
FROM "Worker" w
|
||||
LEFT JOIN "_ActionToWorker" aw ON w.id = aw."B"
|
||||
LEFT JOIN "Action" a ON aw."A" = a.id
|
||||
WHERE
|
||||
a."tenantId" = $1::uuid AND
|
||||
w."id" = $2::uuid
|
||||
`
|
||||
|
||||
type GetWorkerActionsByWorkerIdParams struct {
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
Workerid pgtype.UUID `json:"workerid"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetWorkerActionsByWorkerId(ctx context.Context, db DBTX, arg GetWorkerActionsByWorkerIdParams) ([]pgtype.Text, error) {
|
||||
rows, err := db.Query(ctx, getWorkerActionsByWorkerId, arg.Tenantid, arg.Workerid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []pgtype.Text
|
||||
for rows.Next() {
|
||||
var actionid pgtype.Text
|
||||
if err := rows.Scan(&actionid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, actionid)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkerById = `-- name: GetWorkerById :one
|
||||
SELECT
|
||||
workers.id, workers."createdAt", workers."updatedAt", workers."deletedAt", workers."tenantId", workers."lastHeartbeatAt", workers.name, workers."dispatcherId", workers."maxRuns", workers."isActive", workers."lastListenerEstablished", workers."isPaused", workers.type, workers."webhookId",
|
||||
ww."url" AS "webhookUrl"
|
||||
w.id, w."createdAt", w."updatedAt", w."deletedAt", w."tenantId", w."lastHeartbeatAt", w.name, w."dispatcherId", w."maxRuns", w."isActive", w."lastListenerEstablished", w."isPaused", w.type, w."webhookId",
|
||||
ww."url" AS "webhookUrl",
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM "WorkerSemaphoreSlot"
|
||||
WHERE "workerId" = w.id AND "stepRunId" IS NOT NULL
|
||||
) AS filled_slots
|
||||
FROM
|
||||
"Worker" workers
|
||||
"Worker" w
|
||||
LEFT JOIN
|
||||
"WebhookWorker" ww ON workers."webhookId" = ww."id"
|
||||
"WebhookWorker" ww ON w."webhookId" = ww."id"
|
||||
WHERE
|
||||
workers."id" = $1::uuid
|
||||
w."id" = $1::uuid
|
||||
`
|
||||
|
||||
type GetWorkerByIdRow struct {
|
||||
Worker Worker `json:"worker"`
|
||||
WebhookUrl pgtype.Text `json:"webhookUrl"`
|
||||
Worker Worker `json:"worker"`
|
||||
WebhookUrl pgtype.Text `json:"webhookUrl"`
|
||||
FilledSlots int64 `json:"filled_slots"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetWorkerById(ctx context.Context, db DBTX, id pgtype.UUID) (*GetWorkerByIdRow, error) {
|
||||
@@ -139,6 +181,7 @@ func (q *Queries) GetWorkerById(ctx context.Context, db DBTX, id pgtype.UUID) (*
|
||||
&i.Worker.Type,
|
||||
&i.Worker.WebhookId,
|
||||
&i.WebhookUrl,
|
||||
&i.FilledSlots,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
@@ -465,6 +508,7 @@ const listWorkersWithStepCount = `-- name: ListWorkersWithStepCount :many
|
||||
SELECT
|
||||
workers.id, workers."createdAt", workers."updatedAt", workers."deletedAt", workers."tenantId", workers."lastHeartbeatAt", workers.name, workers."dispatcherId", workers."maxRuns", workers."isActive", workers."lastListenerEstablished", workers."isPaused", workers.type, workers."webhookId",
|
||||
ww."url" AS "webhookUrl",
|
||||
ww."id" AS "webhookId",
|
||||
(SELECT COUNT(*) FROM "WorkerSemaphoreSlot" wss WHERE wss."workerId" = workers."id" AND wss."stepRunId" IS NOT NULL) AS "slots"
|
||||
FROM
|
||||
"Worker" workers
|
||||
@@ -495,7 +539,7 @@ WHERE
|
||||
))
|
||||
)
|
||||
GROUP BY
|
||||
workers."id", ww."url"
|
||||
workers."id", ww."url", ww."id"
|
||||
`
|
||||
|
||||
type ListWorkersWithStepCountParams struct {
|
||||
@@ -508,6 +552,7 @@ type ListWorkersWithStepCountParams struct {
|
||||
type ListWorkersWithStepCountRow struct {
|
||||
Worker Worker `json:"worker"`
|
||||
WebhookUrl pgtype.Text `json:"webhookUrl"`
|
||||
WebhookId pgtype.UUID `json:"webhookId"`
|
||||
Slots int64 `json:"slots"`
|
||||
}
|
||||
|
||||
@@ -541,6 +586,7 @@ func (q *Queries) ListWorkersWithStepCount(ctx context.Context, db DBTX, arg Lis
|
||||
&i.Worker.Type,
|
||||
&i.Worker.WebhookId,
|
||||
&i.WebhookUrl,
|
||||
&i.WebhookId,
|
||||
&i.Slots,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,7 +3,9 @@ package prisma
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
@@ -43,16 +45,32 @@ func (r *webhookWorkerEngineRepository) ListActiveWebhookWorkers(ctx context.Con
|
||||
return r.queries.ListActiveWebhookWorkers(ctx, r.pool, sqlchelpers.UUIDFromStr(tenantId))
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) UpsertWebhookWorker(ctx context.Context, opts *repository.UpsertWebhookWorkerOpts) (*dbsqlc.WebhookWorker, error) {
|
||||
func (r *webhookWorkerEngineRepository) ListWebhookWorkerRequests(ctx context.Context, webhookWorkerId string) ([]*dbsqlc.WebhookWorkerRequest, error) {
|
||||
return r.queries.ListWebhookWorkerRequests(ctx, r.pool, sqlchelpers.UUIDFromStr(webhookWorkerId))
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) InsertWebhookWorkerRequest(ctx context.Context, webhookWorkerId string, method string, statusCode int32) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return r.queries.InsertWebhookWorkerRequest(ctx, r.pool, dbsqlc.InsertWebhookWorkerRequestParams{
|
||||
Webhookworkerid: sqlchelpers.UUIDFromStr(webhookWorkerId),
|
||||
Method: dbsqlc.WebhookWorkerRequestMethod(method),
|
||||
Statuscode: statusCode,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) CreateWebhookWorker(ctx context.Context, opts *repository.CreateWebhookWorkerOpts) (*dbsqlc.WebhookWorker, error) {
|
||||
|
||||
if err := r.v.Validate(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := dbsqlc.UpsertWebhookWorkerParams{
|
||||
params := dbsqlc.CreateWebhookWorkerParams{
|
||||
Tenantid: sqlchelpers.UUIDFromStr(opts.TenantId),
|
||||
Name: sqlchelpers.TextFromStr(opts.Name),
|
||||
Secret: sqlchelpers.TextFromStr(opts.Secret),
|
||||
Url: sqlchelpers.TextFromStr(opts.URL),
|
||||
Name: opts.Name,
|
||||
Secret: opts.Secret,
|
||||
Url: opts.URL,
|
||||
}
|
||||
|
||||
if opts.Deleted != nil {
|
||||
@@ -67,11 +85,54 @@ func (r *webhookWorkerEngineRepository) UpsertWebhookWorker(ctx context.Context,
|
||||
params.TokenValue = sqlchelpers.TextFromStr(*opts.TokenValue)
|
||||
}
|
||||
|
||||
return r.queries.UpsertWebhookWorker(ctx, r.pool, params)
|
||||
worker, err := r.queries.CreateWebhookWorker(ctx, r.pool, params)
|
||||
|
||||
if err != nil {
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23505" {
|
||||
return nil, repository.ErrDuplicateKey
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return worker, nil
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) DeleteWebhookWorker(ctx context.Context, id string, tenantId string) error {
|
||||
return r.queries.DeleteWebhookWorker(ctx, r.pool, dbsqlc.DeleteWebhookWorkerParams{
|
||||
func (r *webhookWorkerEngineRepository) UpdateWebhookWorkerToken(ctx context.Context, id string, tenantId string, opts *repository.UpdateWebhookWorkerTokenOpts) (*dbsqlc.WebhookWorker, error) {
|
||||
if err := r.v.Validate(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := dbsqlc.UpdateWebhookWorkerTokenParams{
|
||||
ID: sqlchelpers.UUIDFromStr(id),
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
}
|
||||
|
||||
if opts.TokenID != nil {
|
||||
params.TokenId = sqlchelpers.UUIDFromStr(*opts.TokenID)
|
||||
}
|
||||
|
||||
if opts.TokenValue != nil {
|
||||
params.TokenValue = sqlchelpers.TextFromStr(*opts.TokenValue)
|
||||
}
|
||||
|
||||
worker, err := r.queries.UpdateWebhookWorkerToken(ctx, r.pool, params)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return worker, nil
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) SoftDeleteWebhookWorker(ctx context.Context, id string, tenantId string) error {
|
||||
return r.queries.SoftDeleteWebhookWorker(ctx, r.pool, dbsqlc.SoftDeleteWebhookWorkerParams{
|
||||
ID: sqlchelpers.UUIDFromStr(id),
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *webhookWorkerEngineRepository) HardDeleteWebhookWorker(ctx context.Context, id string, tenantId string) error {
|
||||
return r.queries.HardDeleteWebhookWorker(ctx, r.pool, dbsqlc.HardDeleteWebhookWorkerParams{
|
||||
ID: sqlchelpers.UUIDFromStr(id),
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
})
|
||||
|
||||
@@ -45,6 +45,13 @@ func (w *workerAPIRepository) GetWorkerById(workerId string) (*dbsqlc.GetWorkerB
|
||||
return w.queries.GetWorkerById(context.Background(), w.pool, sqlchelpers.UUIDFromStr(workerId))
|
||||
}
|
||||
|
||||
func (w *workerAPIRepository) GetWorkerActionsByWorkerId(tenantid, workerId string) ([]pgtype.Text, error) {
|
||||
return w.queries.GetWorkerActionsByWorkerId(context.Background(), w.pool, dbsqlc.GetWorkerActionsByWorkerIdParams{
|
||||
Workerid: sqlchelpers.UUIDFromStr(workerId),
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantid),
|
||||
})
|
||||
}
|
||||
|
||||
func (w *workerAPIRepository) ListWorkerState(tenantId, workerId string, failed bool) ([]*dbsqlc.ListSemaphoreSlotsWithStateForWorkerRow, []*dbsqlc.ListRecentStepRunsForWorkerRow, error) {
|
||||
slots, err := w.queries.ListSemaphoreSlotsWithStateForWorker(context.Background(), w.pool, dbsqlc.ListSemaphoreSlotsWithStateForWorkerParams{
|
||||
Workerid: sqlchelpers.UUIDFromStr(workerId),
|
||||
|
||||
@@ -2,11 +2,12 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/pkg/repository/prisma/dbsqlc"
|
||||
)
|
||||
|
||||
type UpsertWebhookWorkerOpts struct {
|
||||
type CreateWebhookWorkerOpts struct {
|
||||
Name string
|
||||
URL string `validate:"required,url"`
|
||||
Secret string
|
||||
@@ -16,6 +17,13 @@ type UpsertWebhookWorkerOpts struct {
|
||||
TokenID *string
|
||||
}
|
||||
|
||||
type UpdateWebhookWorkerTokenOpts struct {
|
||||
TokenValue *string
|
||||
TokenID *string
|
||||
}
|
||||
|
||||
var ErrDuplicateKey = fmt.Errorf("duplicate key error")
|
||||
|
||||
type WebhookWorkerEngineRepository interface {
|
||||
// ListWebhookWorkersByPartitionId returns the list of webhook workers for a worker partition
|
||||
ListWebhookWorkersByPartitionId(ctx context.Context, partitionId string) ([]*dbsqlc.WebhookWorker, error)
|
||||
@@ -23,9 +31,21 @@ type WebhookWorkerEngineRepository interface {
|
||||
// ListActiveWebhookWorkers returns the list of active webhook workers for the given tenant
|
||||
ListActiveWebhookWorkers(ctx context.Context, tenantId string) ([]*dbsqlc.WebhookWorker, error)
|
||||
|
||||
// UpsertWebhookWorker creates a new webhook worker with the given options
|
||||
UpsertWebhookWorker(ctx context.Context, opts *UpsertWebhookWorkerOpts) (*dbsqlc.WebhookWorker, error)
|
||||
// ListWebhookWorkerRequests returns the list of webhook worker requests for the given webhook worker id
|
||||
ListWebhookWorkerRequests(ctx context.Context, webhookWorkerId string) ([]*dbsqlc.WebhookWorkerRequest, error)
|
||||
|
||||
// DeleteWebhookWorker deletes a webhook worker with the given id and tenant id
|
||||
DeleteWebhookWorker(ctx context.Context, id string, tenantId string) error
|
||||
// InsertWebhookWorkerRequest inserts a new webhook worker request with the given options
|
||||
InsertWebhookWorkerRequest(ctx context.Context, webhookWorkerId string, method string, statusCode int32) error
|
||||
|
||||
// CreateWebhookWorker creates a new webhook worker with the given options
|
||||
CreateWebhookWorker(ctx context.Context, opts *CreateWebhookWorkerOpts) (*dbsqlc.WebhookWorker, error)
|
||||
|
||||
// UpdateWebhookWorkerToken updates a webhook worker with the given id and tenant id
|
||||
UpdateWebhookWorkerToken(ctx context.Context, id string, tenantId string, opts *UpdateWebhookWorkerTokenOpts) (*dbsqlc.WebhookWorker, error)
|
||||
|
||||
// SoftDeleteWebhookWorker flags a webhook worker for delete with the given id and tenant id
|
||||
SoftDeleteWebhookWorker(ctx context.Context, id string, tenantId string) error
|
||||
|
||||
// HardDeleteWebhookWorker deletes a webhook worker with the given id and tenant id
|
||||
HardDeleteWebhookWorker(ctx context.Context, id string, tenantId string) error
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ type WorkerAPIRepository interface {
|
||||
// ListRecentWorkerStepRuns lists recent step runs for a given worker
|
||||
ListWorkerState(tenantId, workerId string, failed bool) ([]*dbsqlc.ListSemaphoreSlotsWithStateForWorkerRow, []*dbsqlc.ListRecentStepRunsForWorkerRow, error)
|
||||
|
||||
// GetWorkerActionsByWorkerId returns a list of actions for a worker
|
||||
GetWorkerActionsByWorkerId(tenantid, workerId string) ([]pgtype.Text, error)
|
||||
|
||||
// GetWorkerById returns a worker by its id.
|
||||
GetWorkerById(workerId string) (*dbsqlc.GetWorkerByIdRow, error)
|
||||
|
||||
|
||||
@@ -119,7 +119,13 @@ func (w *Worker) sendWebhook(ctx context.Context, action *client.Action, ww Webh
|
||||
Action: action,
|
||||
ActionPayload: string(action.ActionPayload),
|
||||
}
|
||||
_, err := whrequest.Send(ctx, ww.URL, ww.Secret, actionWithPayload)
|
||||
|
||||
_, statusCode, err := whrequest.Send(ctx, ww.URL, ww.Secret, actionWithPayload)
|
||||
|
||||
if statusCode != nil && *statusCode != 200 {
|
||||
w.l.Debug().Msgf("step run %s webhook sent with status code %d", action.StepRunId, *statusCode)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.l.Warn().Msgf("step run %s could not send webhook to %s: %s", action.StepRunId, ww.URL, err)
|
||||
if err := w.markFailed(action, fmt.Errorf("could not send webhook: %w", err)); err != nil {
|
||||
|
||||
19
prisma/migrations/20240823120423_0_42_4/migration.sql
Normal file
19
prisma/migrations/20240823120423_0_42_4/migration.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebhookWorkerRequestMethod" AS ENUM ('GET', 'POST', 'PUT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookWorkerRequest" (
|
||||
"id" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"webhookWorkerId" UUID NOT NULL,
|
||||
"method" "WebhookWorkerRequestMethod" NOT NULL,
|
||||
"statusCode" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "WebhookWorkerRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorkerRequest_id_key" ON "WebhookWorkerRequest"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorkerRequest" ADD CONSTRAINT "WebhookWorkerRequest_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -112,6 +112,29 @@ model WebhookWorker {
|
||||
webhookWorkerWorkflows WebhookWorkerWorkflow[]
|
||||
|
||||
worker Worker?
|
||||
|
||||
requests WebhookWorkerRequest[]
|
||||
}
|
||||
|
||||
enum WebhookWorkerRequestMethod {
|
||||
GET
|
||||
POST
|
||||
PUT
|
||||
}
|
||||
|
||||
model WebhookWorkerRequest {
|
||||
id String @id @unique @default(uuid()) @db.Uuid
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// the parent webhook worker
|
||||
webhookWorker WebhookWorker @relation(fields: [webhookWorkerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
webhookWorkerId String @db.Uuid
|
||||
|
||||
// the request method
|
||||
method WebhookWorkerRequestMethod
|
||||
|
||||
// the request status code
|
||||
statusCode Int
|
||||
}
|
||||
|
||||
model WebhookWorkerWorkflow {
|
||||
|
||||
6
sql/migrations/20240823120430_0.42.4.sql
Normal file
6
sql/migrations/20240823120430_0.42.4.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Create enum type "WebhookWorkerRequestMethod"
|
||||
CREATE TYPE "WebhookWorkerRequestMethod" AS ENUM ('GET', 'POST', 'PUT');
|
||||
-- Create "WebhookWorkerRequest" table
|
||||
CREATE TABLE "WebhookWorkerRequest" ("id" uuid NOT NULL, "createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "webhookWorkerId" uuid NOT NULL, "method" "WebhookWorkerRequestMethod" NOT NULL, "statusCode" integer NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "WebhookWorkerRequest_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker" ("id") ON UPDATE CASCADE ON DELETE CASCADE);
|
||||
-- Create index "WebhookWorkerRequest_id_key" to table: "WebhookWorkerRequest"
|
||||
CREATE UNIQUE INDEX "WebhookWorkerRequest_id_key" ON "WebhookWorkerRequest" ("id");
|
||||
@@ -1,4 +1,4 @@
|
||||
h1:ZCdX1Ul6UwDyP8dEntES43VPqA+bJvD4ht0vwdH0jRs=
|
||||
h1:BXNv7MdMKFP6K9x+yDK4ltddqlcL29+Nvc4PIQC/J00=
|
||||
20240115180414_init.sql h1:Ef3ZyjAHkmJPdGF/dEWCahbwgcg6uGJKnDxW2JCRi2k=
|
||||
20240122014727_v0_6_0.sql h1:o/LdlteAeFgoHJ3e/M4Xnghqt9826IE/Y/h0q95Acuo=
|
||||
20240126235456_v0_7_0.sql h1:KiVzt/hXgQ6esbdC6OMJOOWuYEXmy1yeCpmsVAHTFKs=
|
||||
@@ -47,3 +47,4 @@ h1:ZCdX1Ul6UwDyP8dEntES43VPqA+bJvD4ht0vwdH0jRs=
|
||||
20240812153737_v0.42.1.sql h1:dUADhy9vhbGwdrn+h2KqshcdfSUvSUOlF190Sy6xGhw=
|
||||
20240815151244_v0.42.2.sql h1:t1vDmgBTS6QpPIUH+QSgqGIQQ4a7E2g2eA0wHei4jj4=
|
||||
20240821170947_0.42.3.sql h1:eh6eC0fUreFdyVm0pC2D6B/3Cffo/t9SQjf2Ai+6Eu0=
|
||||
20240823120430_0.42.4.sql h1:kdfT+J0j21YBvohnF5k+qtt+4YU6egi4fLIaReDucmc=
|
||||
|
||||
@@ -37,6 +37,9 @@ CREATE TYPE "TenantResourceLimitAlertType" AS ENUM ('Alarm', 'Exhausted');
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VcsProvider" AS ENUM ('GITHUB');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebhookWorkerRequestMethod" AS ENUM ('GET', 'POST', 'PUT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WorkerLabelComparator" AS ENUM ('EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'GREATER_THAN_OR_EQUAL', 'LESS_THAN', 'LESS_THAN_OR_EQUAL');
|
||||
|
||||
@@ -608,6 +611,17 @@ CREATE TABLE "WebhookWorker" (
|
||||
CONSTRAINT "WebhookWorker_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookWorkerRequest" (
|
||||
"id" UUID NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"webhookWorkerId" UUID NOT NULL,
|
||||
"method" "WebhookWorkerRequestMethod" NOT NULL,
|
||||
"statusCode" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "WebhookWorkerRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookWorkerWorkflow" (
|
||||
"id" UUID NOT NULL,
|
||||
@@ -1098,6 +1112,9 @@ CREATE UNIQUE INDEX "WebhookWorker_id_key" ON "WebhookWorker"("id" ASC);
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorker_url_key" ON "WebhookWorker"("url" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorkerRequest_id_key" ON "WebhookWorkerRequest"("id" ASC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookWorkerWorkflow_id_key" ON "WebhookWorkerWorkflow"("id" ASC);
|
||||
|
||||
@@ -1428,6 +1445,9 @@ ALTER TABLE "WebhookWorker" ADD CONSTRAINT "WebhookWorker_tenantId_fkey" FOREIGN
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorker" ADD CONSTRAINT "WebhookWorker_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES "APIToken"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorkerRequest" ADD CONSTRAINT "WebhookWorkerRequest_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebhookWorkerWorkflow" ADD CONSTRAINT "WebhookWorkerWorkflow_webhookWorkerId_fkey" FOREIGN KEY ("webhookWorkerId") REFERENCES "WebhookWorker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user