Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions backend/cmd/clerk/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"log"

"github.com/generate/selfserve/config"
"github.com/generate/selfserve/internal/service/clerk"
"github.com/generate/selfserve/internal/repository"
"github.com/generate/selfserve/internal/service/clerk"
storage "github.com/generate/selfserve/internal/service/storage/postgres"
"github.com/sethvargo/go-envconfig"
)
Expand All @@ -27,7 +27,7 @@ func main() {
usersRepo := repository.NewUsersRepository(repo.DB)

path := "/users"
err = syncUsers(ctx, cfg.BaseURL + path, cfg.SecretKey, usersRepo)
err = syncUsers(ctx, cfg.BaseURL+path, cfg.SecretKey, usersRepo)
if err != nil {
log.Fatal(err)
}
Expand All @@ -53,4 +53,3 @@ func syncUsers(ctx context.Context, clerkBaseURL string, clerkSecret string,

return nil
}

1 change: 0 additions & 1 deletion backend/cmd/clerk/sync_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package main

import (
Expand Down
10 changes: 5 additions & 5 deletions backend/config/clerk.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package config
package config

type Clerk struct {
BaseURL string `env:"BASE_URL" envDefault:"https://api.clerk.com/v1"`
SecretKey string `env:"SECRET_KEY,required"`
WebhookSignature string `env:"WEBHOOK_SIGNATURE,required"`
}
BaseURL string `env:"BASE_URL" envDefault:"https://api.clerk.com/v1"`
SecretKey string `env:"SECRET_KEY,required"`
WebhookSignature string `env:"WEBHOOK_SIGNATURE,required"`
}
2 changes: 1 addition & 1 deletion backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ type Config struct {
Application `env:",prefix=APP_"`
DB `env:",prefix=DB_"`
LLM `env:",prefix=LLM_"`
Clerk `env:",prefix=CLERK_"`
Clerk `env:",prefix=CLERK_"`
}
65 changes: 55 additions & 10 deletions backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ definitions:
type: object
CreateUser:
properties:
clerk_id:
example: user_123
type: string
department:
example: Engineering
example: Housekeeping
type: string
employee_id:
example: EMP-001
example: EMP-1234
type: string
first_name:
example: John
Expand All @@ -30,13 +33,13 @@ definitions:
example: Doe
type: string
profile_picture:
example: https://...
example: https://example.com/john.jpg
type: string
role:
example: admin
example: Receptionist
type: string
timezone:
example: UTC
example: America/New_York
type: string
type: object
Dev:
Expand Down Expand Up @@ -230,14 +233,17 @@ definitions:
type: object
User:
properties:
clerk_id:
example: user_123
type: string
created_at:
example: "2024-01-01T00:00:00Z"
type: string
department:
example: Engineering
example: Housekeeping
type: string
employee_id:
example: EMP-001
example: EMP-1234
type: string
first_name:
example: John
Expand All @@ -249,13 +255,13 @@ definitions:
example: Doe
type: string
profile_picture:
example: https://...
example: https://example.com/john.jpg
type: string
role:
example: admin
example: Receptionist
type: string
timezone:
example: UTC
example: America/New_York
type: string
updated_at:
example: "2024-01-01T00:00:00Z"
Expand Down Expand Up @@ -561,6 +567,45 @@ paths:
summary: creates a request
tags:
- requests
/request/cursor/{cursor}:
get:
consumes:
- application/json
description: Gets 20 requests starting after the cursor ID, filtered by status
parameters:
- description: Cursor UUID
in: path
name: cursor
required: true
type: string
- description: 'Status filter: pending, assigned, in progress, completed'
in: query
name: status
required: true
type: string
produces:
- application/json
responses:
"200":
description: Returns requests array and next_cursor
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get requests by cursor
tags:
- requests
/request/generate:
post:
consumes:
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/handler/clerk.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package handler

import (
"github.com/generate/selfserve/config"
"github.com/generate/selfserve/config"
"github.com/generate/selfserve/internal/errs"
"github.com/generate/selfserve/internal/models"
storage "github.com/generate/selfserve/internal/service/storage/postgres"
Expand Down
47 changes: 47 additions & 0 deletions backend/internal/handler/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,53 @@ func validateGenerateRequest(incoming *models.GenerateRequestInput) error {
return nil
}

// GetRequestByCursor godoc
// @Summary Get requests by cursor
// @Description Gets 20 requests starting after the cursor ID, filtered by status
// @Tags requests
// @Accept json
// @Produce json
// @Param cursor path string true "Cursor UUID"
// @Param status query string true "Status filter: pending, assigned, in progress, completed"
// @Success 200 {object} map[string]interface{} "Returns requests array and next_cursor"
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /request/cursor/{cursor} [get]
func (r *RequestsHandler) GetRequestByCursor(c *fiber.Ctx) error {
cursor := c.Params("cursor")
status := c.Query("status")
if !validUUID(cursor) {
return errs.BadRequest("cursor is not a valid request UUID")
}

//QUESTION FOR REVIEWER: how do we want to represent status?
// should we make an enum AND how are we defining status in our db

validStatuses := map[string]struct{}{
"pending": {},
"assigned": {},
"in progress": {},
"completed": {},
}

if _, ok := validStatuses[status]; !ok {
return errs.BadRequest("Status must be one of: pending, assigned, in progress, completed")
}

requests, nextCursor, err := r.RequestRepository.FindRequestsByCursor(c.Context(), cursor, status)
if err != nil {
if errors.Is(err, errs.ErrNotFoundInDB) {
return errs.NotFound("request cursor id", "cursor", cursor)
}
return c.SendStatus(fiber.ErrInternalServerError.Code)
}

return c.JSON(fiber.Map{
"requests": requests,
"next_cursor": nextCursor,
})
}

// GenerateRequest godoc
// @Summary generates a request
// @Description Generates a request using AI
Expand Down
149 changes: 147 additions & 2 deletions backend/internal/handler/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import (
)

type mockRequestRepository struct {
makeRequestFunc func(ctx context.Context, req *models.Request) (*models.Request, error)
findRequestFunc func(ctx context.Context, id string) (*models.Request, error)
makeRequestFunc func(ctx context.Context, req *models.Request) (*models.Request, error)
findRequestFunc func(ctx context.Context, id string) (*models.Request, error)
findRequestsByCursorFunc func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error)
}

func (m *mockRequestRepository) InsertRequest(ctx context.Context, req *models.Request) (*models.Request, error) {
Expand All @@ -30,6 +31,10 @@ func (m *mockRequestRepository) FindRequest(ctx context.Context, id string) (*mo
return m.findRequestFunc(ctx, id)
}

func (m *mockRequestRepository) FindRequestsByCursor(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return m.findRequestsByCursorFunc(ctx, cursor, status)
}

type mockLLMService struct {
runGenerateRequestFunc func(ctx context.Context, input aiflows.GenerateRequestInput) (aiflows.GenerateRequestOutput, error)
}
Expand Down Expand Up @@ -705,3 +710,143 @@ func TestRequestHandler_Generate_Request(t *testing.T) {
assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", capturedRequest.HotelID)
})
}

func TestRequestHandler_GetRequestByCursor(t *testing.T) {
t.Parallel()

t.Run("returns 200 with requests and next cursor", func(t *testing.T) {
t.Parallel()

mock := &mockRequestRepository{
findRequestsByCursorFunc: func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return []*models.Request{
{
ID: "530e8400-e458-41d4-a716-446655440001",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
MakeRequest: models.MakeRequest{
HotelID: "521e8400-e458-41d4-a716-446655440000",
Name: "room cleaning",
RequestType: "recurring",
Status: "pending",
Priority: "urgent",
},
},
{
ID: "530e8400-e458-41d4-a716-446655440002",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
MakeRequest: models.MakeRequest{
HotelID: "521e8400-e458-41d4-a716-446655440000",
Name: "towel request",
RequestType: "one-time",
Status: "pending",
Priority: "normal",
},
},
}, "530e8400-e458-41d4-a716-446655440002", nil
},
}

app := fiber.New()
h := NewRequestsHandler(mock, nil)
app.Get("/request/cursor/:cursor", h.GetRequestByCursor)

req := httptest.NewRequest("GET", "/request/cursor/530e8400-e458-41d4-a716-446655440000?status=pending", nil)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 200, resp.StatusCode)

body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "530e8400-e458-41d4-a716-446655440001")
assert.Contains(t, string(body), "530e8400-e458-41d4-a716-446655440002")
assert.Contains(t, string(body), "next_cursor")
})

t.Run("returns 400 when cursor is not a valid UUID", func(t *testing.T) {
t.Parallel()

mock := &mockRequestRepository{
findRequestsByCursorFunc: func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return nil, "", errors.New("should not be called")
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRequestsHandler(mock, nil)
app.Get("/request/cursor/:cursor", h.GetRequestByCursor)

req := httptest.NewRequest("GET", "/request/cursor/notaUUID?status=pending", nil)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 400, resp.StatusCode)

body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "cursor")
})

t.Run("returns 400 when status is invalid", func(t *testing.T) {
t.Parallel()

mock := &mockRequestRepository{
findRequestsByCursorFunc: func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return nil, "", errors.New("should not be called")
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRequestsHandler(mock, nil)
app.Get("/request/cursor/:cursor", h.GetRequestByCursor)

req := httptest.NewRequest("GET", "/request/cursor/530e8400-e458-41d4-a716-446655440000?status=invalid", nil)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 400, resp.StatusCode)

body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "Status")
})

t.Run("returns 404 when not found in db", func(t *testing.T) {
t.Parallel()

mock := &mockRequestRepository{
findRequestsByCursorFunc: func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return nil, "", errs.ErrNotFoundInDB
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRequestsHandler(mock, nil)
app.Get("/request/cursor/:cursor", h.GetRequestByCursor)

req := httptest.NewRequest("GET", "/request/cursor/530e8400-e458-41d4-a716-446655440000?status=pending", nil)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 404, resp.StatusCode)
})

t.Run("returns 500 on db error", func(t *testing.T) {
t.Parallel()

mock := &mockRequestRepository{
findRequestsByCursorFunc: func(ctx context.Context, cursor string, status string) ([]*models.Request, string, error) {
return nil, "", errors.New("db connection failed")
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewRequestsHandler(mock, nil)
app.Get("/request/cursor/:cursor", h.GetRequestByCursor)

req := httptest.NewRequest("GET", "/request/cursor/530e8400-e458-41d4-a716-446655440000?status=pending", nil)
resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 500, resp.StatusCode)
})
}
Loading
Loading