diff --git a/make_release.sh b/make_release.sh index 287f0be..82c5726 100644 --- a/make_release.sh +++ b/make_release.sh @@ -15,12 +15,12 @@ if [[ $make_release =~ ^[Yy]$ ]]; then fi # Create an annotated tag - git tag -a "$version" -m "Released Core $version" + git tag -a "$version" -m "Released $version" # Push the tag to the remote repository git push origin "$version" - echo "Tag $version created for Core and pushed to the remote repository." + echo "Tag $version created and pushed to the remote repository." else echo "No release version created." fi diff --git a/pkg/resolvespec/context.go b/pkg/resolvespec/context.go new file mode 100644 index 0000000..3c081cc --- /dev/null +++ b/pkg/resolvespec/context.go @@ -0,0 +1,85 @@ +package resolvespec + +import ( + "context" +) + +// Context keys for request-scoped data +type contextKey string + +const ( + contextKeySchema contextKey = "schema" + contextKeyEntity contextKey = "entity" + contextKeyTableName contextKey = "tableName" + contextKeyModel contextKey = "model" + contextKeyModelPtr contextKey = "modelPtr" +) + +// WithSchema adds schema to context +func WithSchema(ctx context.Context, schema string) context.Context { + return context.WithValue(ctx, contextKeySchema, schema) +} + +// GetSchema retrieves schema from context +func GetSchema(ctx context.Context) string { + if v := ctx.Value(contextKeySchema); v != nil { + return v.(string) + } + return "" +} + +// WithEntity adds entity to context +func WithEntity(ctx context.Context, entity string) context.Context { + return context.WithValue(ctx, contextKeyEntity, entity) +} + +// GetEntity retrieves entity from context +func GetEntity(ctx context.Context) string { + if v := ctx.Value(contextKeyEntity); v != nil { + return v.(string) + } + return "" +} + +// WithTableName adds table name to context +func WithTableName(ctx context.Context, tableName string) context.Context { + return context.WithValue(ctx, contextKeyTableName, tableName) +} + +// GetTableName retrieves table name from context +func GetTableName(ctx context.Context) string { + if v := ctx.Value(contextKeyTableName); v != nil { + return v.(string) + } + return "" +} + +// WithModel adds model to context +func WithModel(ctx context.Context, model interface{}) context.Context { + return context.WithValue(ctx, contextKeyModel, model) +} + +// GetModel retrieves model from context +func GetModel(ctx context.Context) interface{} { + return ctx.Value(contextKeyModel) +} + +// WithModelPtr adds model pointer to context +func WithModelPtr(ctx context.Context, modelPtr interface{}) context.Context { + return context.WithValue(ctx, contextKeyModelPtr, modelPtr) +} + +// GetModelPtr retrieves model pointer from context +func GetModelPtr(ctx context.Context) interface{} { + return ctx.Value(contextKeyModelPtr) +} + +// WithRequestData adds all request-scoped data to context at once +func WithRequestData(ctx context.Context, schema, entity, tableName string, model, modelPtr interface{}) context.Context { + ctx = WithSchema(ctx, schema) + ctx = WithEntity(ctx, entity) + ctx = WithTableName(ctx, tableName) + ctx = WithModel(ctx, model) + ctx = WithModelPtr(ctx, modelPtr) + return ctx +} diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index 9a274c9..5c617b8 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -50,15 +50,30 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity) + // Get model and populate context with request-scoped data + model, err := h.registry.GetModelByEntity(schema, entity) + if err != nil { + logger.Error("Invalid entity: %v", err) + h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) + return + } + + // Create a pointer to the model type for database operations + modelPtr := reflect.New(reflect.TypeOf(model)).Interface() + tableName := h.getTableName(schema, entity, model) + + // Add request-scoped data to context + ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr) + switch req.Operation { case "read": - h.handleRead(ctx, w, schema, entity, id, req.Options) + h.handleRead(ctx, w, id, req.Options) case "create": - h.handleCreate(ctx, w, schema, entity, req.Data, req.Options) + h.handleCreate(ctx, w, req.Data, req.Options) case "update": - h.handleUpdate(ctx, w, schema, entity, id, req.ID, req.Data, req.Options) + h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options) case "delete": - h.handleDelete(ctx, w, schema, entity, id) + h.handleDelete(ctx, w, id) default: logger.Error("Invalid operation: %s", req.Operation) h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil) @@ -83,24 +98,16 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma h.sendResponse(w, metadata, nil) } -func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schema, entity, id string, options common.RequestOptions) { +func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options common.RequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + model := GetModel(ctx) + modelPtr := GetModelPtr(ctx) + logger.Info("Reading records from %s.%s", schema, entity) - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Error("Invalid entity: %v", err) - h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) - return - } - - // Model is now a non-pointer struct, create a pointer instance for ORM - modelType := reflect.TypeOf(model) - modelPtr := reflect.New(modelType).Interface() - query := h.db.NewSelect().Model(modelPtr) - - // Get table name - tableName := h.getTableName(schema, entity, model) query = query.Table(tableName) // Apply column selection @@ -154,7 +161,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem if id != "" { logger.Debug("Querying single record with ID: %s", id) // Create a pointer to the struct type for scanning - singleResult := reflect.New(modelType).Interface() + singleResult := reflect.New(reflect.TypeOf(model)).Interface() query = query.Where("id = ?", id) if err := query.Scan(ctx, singleResult); err != nil { logger.Error("Error querying record: %v", err) @@ -164,8 +171,8 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem result = singleResult } else { logger.Debug("Querying multiple records") - // Create a slice of the struct type (not pointers) - sliceType := reflect.SliceOf(modelType) + // Create a slice of pointers to the model type + sliceType := reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(model))) results := reflect.New(sliceType).Interface() if err := query.Scan(ctx, results); err != nil { @@ -195,17 +202,13 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem }) } -func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, schema, entity string, data interface{}, options common.RequestOptions) { +func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, data interface{}, options common.RequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + logger.Info("Creating records for %s.%s", schema, entity) - // Get the model to determine the actual table name - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Warn("Model not found, using default table name") - model = nil - } - - tableName := h.getTableName(schema, entity, model) query := h.db.NewInsert().Table(tableName) switch v := data.(type) { @@ -275,18 +278,13 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch } } -func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) { +func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + logger.Info("Updating records for %s.%s", schema, entity) - // Get the model to determine the actual table name - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Warn("Model not found, using default table name") - // Fallback to entity name (without schema for SQLite compatibility) - model = nil - } - - tableName := h.getTableName(schema, entity, model) query := h.db.NewUpdate().Table(tableName) switch updates := data.(type) { @@ -330,7 +328,11 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch h.sendResponse(w, data, nil) } -func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, schema, entity, id string) { +func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + logger.Info("Deleting records from %s.%s", schema, entity) if id == "" { @@ -339,14 +341,6 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, sch return } - // Get the model to determine the actual table name - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Warn("Model not found, using default table name") - model = nil - } - - tableName := h.getTableName(schema, entity, model) query := h.db.NewDelete().Table(tableName).Where("id = ?", id) result, err := query.Exec(ctx) diff --git a/pkg/restheadspec/context.go b/pkg/restheadspec/context.go new file mode 100644 index 0000000..820ef21 --- /dev/null +++ b/pkg/restheadspec/context.go @@ -0,0 +1,85 @@ +package restheadspec + +import ( + "context" +) + +// Context keys for request-scoped data +type contextKey string + +const ( + contextKeySchema contextKey = "schema" + contextKeyEntity contextKey = "entity" + contextKeyTableName contextKey = "tableName" + contextKeyModel contextKey = "model" + contextKeyModelPtr contextKey = "modelPtr" +) + +// WithSchema adds schema to context +func WithSchema(ctx context.Context, schema string) context.Context { + return context.WithValue(ctx, contextKeySchema, schema) +} + +// GetSchema retrieves schema from context +func GetSchema(ctx context.Context) string { + if v := ctx.Value(contextKeySchema); v != nil { + return v.(string) + } + return "" +} + +// WithEntity adds entity to context +func WithEntity(ctx context.Context, entity string) context.Context { + return context.WithValue(ctx, contextKeyEntity, entity) +} + +// GetEntity retrieves entity from context +func GetEntity(ctx context.Context) string { + if v := ctx.Value(contextKeyEntity); v != nil { + return v.(string) + } + return "" +} + +// WithTableName adds table name to context +func WithTableName(ctx context.Context, tableName string) context.Context { + return context.WithValue(ctx, contextKeyTableName, tableName) +} + +// GetTableName retrieves table name from context +func GetTableName(ctx context.Context) string { + if v := ctx.Value(contextKeyTableName); v != nil { + return v.(string) + } + return "" +} + +// WithModel adds model to context +func WithModel(ctx context.Context, model interface{}) context.Context { + return context.WithValue(ctx, contextKeyModel, model) +} + +// GetModel retrieves model from context +func GetModel(ctx context.Context) interface{} { + return ctx.Value(contextKeyModel) +} + +// WithModelPtr adds model pointer to context +func WithModelPtr(ctx context.Context, modelPtr interface{}) context.Context { + return context.WithValue(ctx, contextKeyModelPtr, modelPtr) +} + +// GetModelPtr retrieves model pointer from context +func GetModelPtr(ctx context.Context) interface{} { + return ctx.Value(contextKeyModelPtr) +} + +// WithRequestData adds all request-scoped data to context at once +func WithRequestData(ctx context.Context, schema, entity, tableName string, model, modelPtr interface{}) context.Context { + ctx = WithSchema(ctx, schema) + ctx = WithEntity(ctx, entity) + ctx = WithTableName(ctx, tableName) + ctx = WithModel(ctx, model) + ctx = WithModelPtr(ctx, modelPtr) + return ctx +} diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index c98b2f0..de27f92 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -44,14 +44,28 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s logger.Info("Handling %s request for %s.%s", method, schema, entity) + // Get model and populate context with request-scoped data + model, err := h.registry.GetModelByEntity(schema, entity) + if err != nil { + logger.Error("Invalid entity: %v", err) + h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) + return + } + + modelPtr := reflect.New(reflect.TypeOf(model)).Interface() + tableName := h.getTableName(schema, entity, model) + + // Add request-scoped data to context + ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr) + switch method { case "GET": if id != "" { // GET with ID - read single record - h.handleRead(ctx, w, schema, entity, id, options) + h.handleRead(ctx, w, id, options) } else { // GET without ID - read multiple records - h.handleRead(ctx, w, schema, entity, "", options) + h.handleRead(ctx, w, "", options) } case "POST": // Create operation @@ -67,7 +81,7 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err) return } - h.handleCreate(ctx, w, schema, entity, data, options) + h.handleCreate(ctx, w, data, options) case "PUT", "PATCH": // Update operation body, err := r.Body() @@ -82,9 +96,9 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err) return } - h.handleUpdate(ctx, w, schema, entity, id, nil, data, options) + h.handleUpdate(ctx, w, id, nil, data, options) case "DELETE": - h.handleDelete(ctx, w, schema, entity, id) + h.handleDelete(ctx, w, id) default: logger.Error("Invalid HTTP method: %s", method) h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil) @@ -111,20 +125,15 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma // parseOptionsFromHeaders is now implemented in headers.go -func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schema, entity, id string, options ExtendedRequestOptions) { +func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options ExtendedRequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + modelPtr := GetModelPtr(ctx) + logger.Info("Reading records from %s.%s", schema, entity) - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Error("Invalid entity: %v", err) - h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) - return - } - - query := h.db.NewSelect().Model(model) - - // Get table name - tableName := h.getTableName(schema, entity, model) + query := h.db.NewSelect().Model(modelPtr) query = query.Table(tableName) // Apply column selection @@ -214,8 +223,9 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem query = query.Offset(*options.Offset) } - // Execute query - resultSlice := reflect.New(reflect.SliceOf(reflect.TypeOf(model))).Interface() + // Execute query - create a slice of pointers to the model type + model := GetModel(ctx) + resultSlice := reflect.New(reflect.SliceOf(reflect.PointerTo(reflect.TypeOf(model)))).Interface() if err := query.Scan(ctx, resultSlice); err != nil { logger.Error("Error executing query: %v", err) h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err) @@ -241,18 +251,14 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, schem h.sendFormattedResponse(w, resultSlice, metadata, options) } -func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, schema, entity string, data interface{}, options ExtendedRequestOptions) { +func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, data interface{}, options ExtendedRequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + model := GetModel(ctx) + logger.Info("Creating record in %s.%s", schema, entity) - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Error("Invalid entity: %v", err) - h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) - return - } - - tableName := h.getTableName(schema, entity, model) - // Handle batch creation dataValue := reflect.ValueOf(data) if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array { @@ -263,8 +269,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch for i := 0; i < dataValue.Len(); i++ { item := dataValue.Index(i).Interface() - // Convert item to model type - modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface() + // Convert item to model type - create a pointer to the model + modelValue := reflect.New(reflect.TypeOf(model)).Interface() jsonData, err := json.Marshal(item) if err != nil { return fmt.Errorf("failed to marshal item: %w", err) @@ -291,8 +297,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch return } - // Single record creation - modelValue := reflect.New(reflect.TypeOf(model).Elem()).Interface() + // Single record creation - create a pointer to the model + modelValue := reflect.New(reflect.TypeOf(model)).Interface() jsonData, err := json.Marshal(data) if err != nil { logger.Error("Error marshaling data: %v", err) @@ -315,18 +321,13 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, sch h.sendResponse(w, modelValue, nil) } -func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, schema, entity, id string, idPtr *int64, data interface{}, options ExtendedRequestOptions) { +func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id string, idPtr *int64, data interface{}, options ExtendedRequestOptions) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + logger.Info("Updating record in %s.%s", schema, entity) - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Error("Invalid entity: %v", err) - h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) - return - } - - tableName := h.getTableName(schema, entity, model) - // Convert data to map dataMap, ok := data.(map[string]interface{}) if !ok { @@ -367,18 +368,13 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, sch }, nil) } -func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, schema, entity, id string) { +func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) { + schema := GetSchema(ctx) + entity := GetEntity(ctx) + tableName := GetTableName(ctx) + logger.Info("Deleting record from %s.%s", schema, entity) - model, err := h.registry.GetModelByEntity(schema, entity) - if err != nil { - logger.Error("Invalid entity: %v", err) - h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err) - return - } - - tableName := h.getTableName(schema, entity, model) - query := h.db.NewDelete().Table(tableName) if id == "" { diff --git a/pkg/restheadspec/restheadspec.go b/pkg/restheadspec/restheadspec.go index 6e7cadd..c050b74 100644 --- a/pkg/restheadspec/restheadspec.go +++ b/pkg/restheadspec/restheadspec.go @@ -1,3 +1,55 @@ +// Package restheadspec provides the Rest Header Spec API framework. +// +// Rest Header Spec (restheadspec) is a RESTful API framework that reads query options, +// filters, sorting, pagination, and other parameters from HTTP headers instead of +// request bodies or query parameters. This approach provides a clean separation between +// data and metadata in API requests. +// +// # Key Features +// +// - Header-based API configuration: All query options are passed via HTTP headers +// - Database-agnostic: Works with both GORM and Bun ORM through adapters +// - Router-agnostic: Supports multiple HTTP routers (Mux, BunRouter, etc.) +// - Advanced filtering: Supports complex filter operations (eq, gt, lt, like, between, etc.) +// - Pagination and sorting: Built-in support for limit, offset, and multi-column sorting +// - Preloading and expansion: Support for eager loading relationships +// - Multiple response formats: Default, simple, and Syncfusion formats +// +// # HTTP Headers +// +// The following headers are supported for configuring API requests: +// +// - X-Filters: JSON array of filter conditions +// - X-Columns: Comma-separated list of columns to select +// - X-Sort: JSON array of sort specifications +// - X-Limit: Maximum number of records to return +// - X-Offset: Number of records to skip +// - X-Preload: Comma-separated list of relations to preload +// - X-Expand: Comma-separated list of relations to expand (LEFT JOIN) +// - X-Distinct: Boolean to enable DISTINCT queries +// - X-Skip-Count: Boolean to skip total count query +// - X-Response-Format: Response format (detail, simple, syncfusion) +// - X-Clean-JSON: Boolean to remove null/empty fields +// - X-Custom-SQL-Where: Custom SQL WHERE clause (AND) +// - X-Custom-SQL-Or: Custom SQL WHERE clause (OR) +// +// # Usage Example +// +// // Create a handler with GORM +// handler := restheadspec.NewHandlerWithGORM(db) +// +// // Register models +// handler.Registry.RegisterModel("users", User{}) +// +// // Setup routes with Mux +// muxRouter := mux.NewRouter() +// restheadspec.SetupMuxRoutes(muxRouter, handler) +// +// // Make a request with headers +// // GET /public/users +// // X-Filters: [{"column":"age","operator":"gt","value":18}] +// // X-Sort: [{"column":"name","direction":"asc"}] +// // X-Limit: 10 package restheadspec import (