Skip to content

Commit 90ec1dd

Browse files
jakubnoe2b-bot[bot]
authored andcommitted
feat(api): cap the unpaginated template list at 5000 templates
GitOrigin-RevId: b1277f394e581b634dc9e65685b5ebb403b93650
1 parent f1f79ff commit 90ec1dd

9 files changed

Lines changed: 205 additions & 106 deletions

File tree

‎packages/api/internal/api/api.gen.go‎

Lines changed: 109 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/api/internal/handlers/templates_list.go‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package handlers
22

33
import (
4+
"fmt"
45
"net/http"
56

67
"github.com/gin-gonic/gin"
78

89
"github.com/e2b-dev/infra/packages/api/internal/api"
10+
"github.com/e2b-dev/infra/packages/db/queries"
911
"github.com/e2b-dev/infra/packages/shared/pkg/telemetry"
1012
)
1113

14+
// templatesUnboundedMaxCount caps GET /templates, which returns every template
15+
// in one response; larger teams must page through GET /v2/templates.
16+
const templatesUnboundedMaxCount = 5000
17+
1218
// GetTemplates serves to list templates (e.g. in CLI)
1319
func (a *APIStore) GetTemplates(c *gin.Context, params api.GetTemplatesParams) {
1420
ctx := c.Request.Context()
@@ -34,14 +40,24 @@ func (a *APIStore) GetTemplates(c *gin.Context, params api.GetTemplatesParams) {
3440
telemetry.WithTeamID(team.ID.String()),
3541
)
3642

37-
envs, err := a.sqlcDB.GetTeamTemplates(ctx, team.ID)
43+
envs, err := a.sqlcDB.GetTeamTemplates(ctx, queries.GetTeamTemplatesParams{
44+
TeamID: team.ID,
45+
LimitPlusOne: templatesUnboundedMaxCount + 1,
46+
})
3847
if err != nil {
3948
a.sendAPIStoreError(c, http.StatusInternalServerError, "Error when getting templates")
4049
telemetry.ReportCriticalError(ctx, "error when getting templates", err)
4150

4251
return
4352
}
4453

54+
if len(envs) > templatesUnboundedMaxCount {
55+
a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Team has more than %d templates; update to the latest E2B SDK or list them with GET /v2/templates", templatesUnboundedMaxCount))
56+
telemetry.ReportEvent(ctx, "template list exceeds the unbounded limit")
57+
58+
return
59+
}
60+
4561
telemetry.ReportEvent(ctx, "listed environments")
4662

4763
a.posthog.IdentifyAnalyticsTeam(ctx, team.ID.String(), team.Name)

‎packages/api/internal/handlers/templates_list_test.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package handlers
22

33
import (
4+
"encoding/json"
45
"net/http"
56
"net/http/httptest"
67
"testing"
78

89
"github.com/gin-gonic/gin"
910
"github.com/google/uuid"
11+
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113

1214
analyticscollector "github.com/e2b-dev/infra/packages/api/internal/analytics_collector"
@@ -63,3 +65,46 @@ func TestTemplateListsAcceptPublicProjectID(t *testing.T) {
6365
})
6466
}
6567
}
68+
69+
func TestGetTemplatesRejectsTeamsAboveTheCap(t *testing.T) {
70+
t.Parallel()
71+
72+
for _, tc := range []struct {
73+
name string
74+
templates int
75+
wantCode int
76+
}{
77+
{name: "at the cap", templates: templatesUnboundedMaxCount, wantCode: http.StatusOK},
78+
{name: "above the cap", templates: templatesUnboundedMaxCount + 1, wantCode: http.StatusBadRequest},
79+
} {
80+
t.Run(tc.name, func(t *testing.T) {
81+
t.Parallel()
82+
83+
db := testutils.SetupDatabase(t)
84+
teamID := testutils.CreateTestTeam(t, db)
85+
require.NoError(t, db.SqlcClient.TestsRawSQL(t.Context(),
86+
`INSERT INTO public.envs (id, team_id, public, updated_at, source)
87+
SELECT 'template-' || g, $1, false, NOW(), 'template' FROM generate_series(1, $2::int) AS g`,
88+
teamID, tc.templates,
89+
))
90+
91+
posthogClient, err := analyticscollector.NewPosthogClient(t.Context(), "")
92+
require.NoError(t, err)
93+
store := &APIStore{sqlcDB: db.SqlcClient, posthog: posthogClient}
94+
95+
recorder := httptest.NewRecorder()
96+
c, _ := gin.CreateTestContext(recorder)
97+
c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/templates", nil)
98+
auth.SetTeamInfoForTest(t, c, &types.Team{Team: &authqueries.Team{ID: teamID}})
99+
100+
store.GetTemplates(c, api.GetTemplatesParams{})
101+
102+
require.Equal(t, tc.wantCode, recorder.Code, recorder.Body.String())
103+
if tc.wantCode == http.StatusOK {
104+
var templates []api.Template
105+
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &templates))
106+
assert.Len(t, templates, tc.templates)
107+
}
108+
})
109+
}
110+
}

‎packages/db/pkg/tests/templates/delete_template_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestDeleteTemplate_SoftDeletesEnvAndPreservesStructure(t *testing.T) {
5151
assert.True(t, testutils.GetEnvBuildByID(t, ctx, db, buildID), "build row must be preserved")
5252
assert.NotEmpty(t, testutils.GetBuildAssignments(t, ctx, db, templateID), "build assignment must be preserved")
5353

54-
templates, err := db.SqlcClient.GetTeamTemplates(ctx, teamID)
54+
templates, err := db.SqlcClient.GetTeamTemplates(ctx, queries.GetTeamTemplatesParams{TeamID: teamID, LimitPlusOne: 10})
5555
require.NoError(t, err)
5656
assert.Empty(t, templates, "soft-deleted template must not be listed")
5757

‎packages/db/pkg/tests/templates/template_assignment_ordering_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ func TestGetTeamTemplates_AssignmentOrderDifferentFromBuildOrder(t *testing.T) {
163163
testutils.CreateTestBuildAssignment(t, ctx, db, templateID, build1ID, "default")
164164

165165
// GetTeamTemplates should return build1 (latest assignment)
166-
results, err := db.SqlcClient.GetTeamTemplates(ctx, teamID)
166+
results, err := db.SqlcClient.GetTeamTemplates(ctx, queries.GetTeamTemplatesParams{TeamID: teamID, LimitPlusOne: 10})
167167
require.NoError(t, err)
168168
require.Len(t, results, 1)
169169

‎packages/db/queries/get_team_templates.sql‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ LEFT JOIN LATERAL (
3535
LIMIT 1
3636
) eb ON TRUE
3737
WHERE
38-
e.team_id = $1
38+
e.team_id = sqlc.arg(team_id)
3939
AND e.source = 'template'
40-
ORDER BY e.created_at ASC;
40+
ORDER BY e.created_at ASC
41+
LIMIT sqlc.arg(limit_plus_one)::int;

‎packages/db/queries/get_team_templates.sql.go‎

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎spec/openapi.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5008,7 +5008,7 @@ paths:
50085008
get:
50095009
summary: List templates
50105010
x-api-group: list
5011-
description: List all templates
5011+
description: List all templates. Returns 400 when the team has more than 5000 templates; use /v2/templates to page through them.
50125012
deprecated: true
50135013
tags: [templates]
50145014
security:
@@ -5035,6 +5035,8 @@ paths:
50355035
type: array
50365036
items:
50375037
$ref: "#/components/schemas/Template"
5038+
"400":
5039+
$ref: "#/components/responses/400"
50385040
"401":
50395041
$ref: "#/components/responses/401"
50405042
"429":

‎tests/integration/internal/api/generated.go‎

Lines changed: 18 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)