Skip to content

Commit 6f6bc42

Browse files
committed
feat(dev-server): fix CORS and routing for browser-based SDKs
- Fix CORS preflight handling for SDK endpoints (evalx, eval, goals) - Fix evalx/eval routing with subrouter PathPrefix corrections - Add client-side ID to project key mapping with database caching - Add database migration for client_side_id column - Implement GetProjectKeyFromClientSideId middleware - Add comprehensive tests for new functionality - Make client-side ID fetching best-effort to prevent failures
1 parent 2199de8 commit 6f6bc42

File tree

12 files changed

+306
-101
lines changed

12 files changed

+306
-101
lines changed

internal/dev_server/db/sqlite.go

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"io"
88
"os"
9+
"strings"
910

1011
_ "github.com/mattn/go-sqlite3"
1112
"github.com/pkg/errors"
@@ -47,12 +48,12 @@ func (s *Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project,
4748
var flagStateData string
4849

4950
row := s.database.QueryRowContext(ctx, `
50-
SELECT key, source_environment_key, context, last_sync_time, flag_state
51+
SELECT key, source_environment_key, client_side_id, context, last_sync_time, flag_state
5152
FROM projects
5253
WHERE key = ?
5354
`, key)
5455

55-
if err := row.Scan(&project.Key, &project.SourceEnvironmentKey, &contextData, &project.LastSyncTime, &flagStateData); err != nil {
56+
if err := row.Scan(&project.Key, &project.SourceEnvironmentKey, &project.ClientSideId, &contextData, &project.LastSyncTime, &flagStateData); err != nil {
5657
if errors.Is(err, sql.ErrNoRows) {
5758
return nil, model.NewErrNotFound("project", key)
5859
}
@@ -72,6 +73,37 @@ func (s *Sqlite) GetDevProject(ctx context.Context, key string) (*model.Project,
7273
return &project, nil
7374
}
7475

76+
func (s *Sqlite) GetDevProjectByClientSideId(ctx context.Context, clientSideId string) (*model.Project, error) {
77+
var project model.Project
78+
var contextData string
79+
var flagStateData string
80+
81+
row := s.database.QueryRowContext(ctx, `
82+
SELECT key, source_environment_key, client_side_id, context, last_sync_time, flag_state
83+
FROM projects
84+
WHERE client_side_id = ? AND client_side_id IS NOT NULL
85+
`, clientSideId)
86+
87+
if err := row.Scan(&project.Key, &project.SourceEnvironmentKey, &project.ClientSideId, &contextData, &project.LastSyncTime, &flagStateData); err != nil {
88+
if errors.Is(err, sql.ErrNoRows) {
89+
return nil, model.NewErrNotFound("project", clientSideId)
90+
}
91+
return nil, err
92+
}
93+
94+
// Parse the context JSON string
95+
if err := json.Unmarshal([]byte(contextData), &project.Context); err != nil {
96+
return nil, errors.Wrap(err, "unable to unmarshal context data")
97+
}
98+
99+
// Parse the flag state JSON string
100+
if err := json.Unmarshal([]byte(flagStateData), &project.AllFlagsState); err != nil {
101+
return nil, errors.Wrap(err, "unable to unmarshal flag state data")
102+
}
103+
104+
return &project, nil
105+
}
106+
75107
func (s *Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool, error) {
76108
flagsStateJson, err := json.Marshal(project.AllFlagsState)
77109
if err != nil {
@@ -89,9 +121,9 @@ func (s *Sqlite) UpdateProject(ctx context.Context, project model.Project) (bool
89121
}()
90122
result, err := tx.ExecContext(ctx, `
91123
UPDATE projects
92-
SET flag_state = ?, last_sync_time = ?, context=?, source_environment_key=?
124+
SET flag_state = ?, last_sync_time = ?, context=?, source_environment_key=?, client_side_id=?
93125
WHERE key = ?;
94-
`, flagsStateJson, project.LastSyncTime, project.Context.JSONString(), project.SourceEnvironmentKey, project.Key)
126+
`, flagsStateJson, project.LastSyncTime, project.Context.JSONString(), project.SourceEnvironmentKey, project.ClientSideId, project.Key)
95127
if err != nil {
96128
return false, errors.Wrap(err, "unable to execute update project")
97129
}
@@ -200,11 +232,12 @@ SELECT 1 FROM projects WHERE key = ?
200232
return
201233
}
202234
_, err = tx.Exec(`
203-
INSERT INTO projects (key, source_environment_key, context, last_sync_time, flag_state)
204-
VALUES (?, ?, ?, ?, ?)
235+
INSERT INTO projects (key, source_environment_key, client_side_id, context, last_sync_time, flag_state)
236+
VALUES (?, ?, ?, ?, ?, ?)
205237
`,
206238
project.Key,
207239
project.SourceEnvironmentKey,
240+
project.ClientSideId,
208241
project.Context.JSONString(),
209242
project.LastSyncTime,
210243
string(flagsStateJson),
@@ -429,6 +462,11 @@ var validationQueries = []string{
429462
"SELECT COUNT(1) from available_variations",
430463
}
431464

465+
func isColumnExistsError(err error) bool {
466+
return err != nil && (strings.Contains(err.Error(), "duplicate column name: client_side_id") ||
467+
strings.Contains(err.Error(), "table projects has no column named client_side_id"))
468+
}
469+
432470
func (s *Sqlite) runMigrations(ctx context.Context) error {
433471
tx, err := s.database.BeginTx(ctx, nil)
434472
if err != nil {
@@ -443,6 +481,7 @@ func (s *Sqlite) runMigrations(ctx context.Context) error {
443481
CREATE TABLE IF NOT EXISTS projects (
444482
key text PRIMARY KEY,
445483
source_environment_key text NOT NULL,
484+
client_side_id text,
446485
context text NOT NULL,
447486
last_sync_time timestamp NOT NULL,
448487
flag_state TEXT NOT NULL
@@ -479,5 +518,14 @@ func (s *Sqlite) runMigrations(ctx context.Context) error {
479518
return err
480519
}
481520

521+
// Add client_side_id column to existing projects table if it doesn't exist
522+
_, err = tx.Exec(`ALTER TABLE projects ADD COLUMN client_side_id text`)
523+
if err != nil {
524+
// Ignore error if column already exists
525+
if !isColumnExistsError(err) {
526+
return err
527+
}
528+
}
529+
482530
return tx.Commit()
483531
}

internal/dev_server/dev_server.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,17 @@ func (c LDClient) RunServer(ctx context.Context, serverParams ServerParams) {
6666
r.Handle("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently))
6767
r.PathPrefix("/ui/").Handler(http.StripPrefix("/ui/", ui.AssetHandler))
6868
sdk.BindRoutes(r)
69-
handler := api.HandlerFromMux(apiServer, r)
70-
handler = api.CorsHeadersWithConfig(serverParams.CorsEnabled, serverParams.CorsOrigin)(handler)
69+
70+
// Create API handler with CORS middleware applied only to API routes
71+
var apiMiddlewares []api.MiddlewareFunc
72+
if serverParams.CorsEnabled {
73+
apiMiddlewares = append(apiMiddlewares, api.CorsHeadersWithConfig(true, serverParams.CorsOrigin))
74+
}
75+
handler := api.HandlerWithOptions(apiServer, api.GorillaServerOptions{
76+
BaseRouter: r,
77+
Middlewares: apiMiddlewares,
78+
})
79+
7180
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
7281
handler = handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))(handler)
7382

internal/dev_server/model/mocks/store.go

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

internal/dev_server/model/project.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
type Project struct {
1515
Key string
1616
SourceEnvironmentKey string
17+
ClientSideId *string
1718
Context ldcontext.Context
1819
LastSyncTime time.Time
1920
AllFlagsState FlagsState
@@ -57,6 +58,12 @@ func (project *Project) refreshExternalState(ctx context.Context) error {
5758
return err
5859
}
5960
project.AvailableVariations = availableVariations
61+
62+
// Fetch client-side ID for caching (best-effort; non-fatal if unavailable)
63+
if clientSideId, cidErr := project.fetchClientSideId(ctx); cidErr == nil {
64+
project.ClientSideId = clientSideId
65+
}
66+
6067
return nil
6168
}
6269

@@ -156,3 +163,19 @@ func (project Project) fetchFlagState(ctx context.Context) (FlagsState, error) {
156163
flagsState = FromAllFlags(sdkFlags)
157164
return flagsState, nil
158165
}
166+
167+
func (project Project) fetchClientSideId(ctx context.Context) (*string, error) {
168+
apiAdapter := adapters.GetApi(ctx)
169+
environments, err := apiAdapter.GetProjectEnvironments(ctx, project.Key, "", nil)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
for _, env := range environments {
175+
if env.Key == project.SourceEnvironmentKey {
176+
return &env.Id, nil
177+
}
178+
}
179+
180+
return nil, errors.New("client-side ID not found for environment " + project.SourceEnvironmentKey)
181+
}

0 commit comments

Comments
 (0)