-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
set default optional foreign key UUIDs to null on full update
- Loading branch information
Showing
5 changed files
with
374 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package pg | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/kataras/pg/desc" | ||
) | ||
|
||
// Exampler is an interface used by testing to generate example values for a specific struct field. | ||
type Exampler interface { | ||
ListExamples() any | ||
} | ||
|
||
type HTTPController[T any] struct { | ||
repository *Repository[T] | ||
primaryKeyType desc.DataType | ||
|
||
// ErrorHandler defaults to the PG's error handler. It can be customized for this controller. | ||
// Setting this to nil will panic the application on the first error. | ||
ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) | ||
|
||
// AfterPayloadRead is called after the payload is read. | ||
// It can be used to validate the payload or set default fields based on the request Context. | ||
AfterPayloadRead func(w http.ResponseWriter, r *http.Request, payload T) (T, bool) | ||
} | ||
|
||
func NewHTTPController[T any](repository *Repository[T]) *HTTPController[T] { | ||
return &HTTPController[T]{ | ||
repository: repository, | ||
} | ||
} | ||
|
||
type ( | ||
jsonSchema[T any] struct { | ||
Description string `json:"description,omitempty"` | ||
Types []jsonSchemaFieldType `json:"types,omitempty"` | ||
Fields []jsonSchemaField `json:"fields"` | ||
} | ||
|
||
jsonSchemaFieldType struct { | ||
Name string `json:"name"` | ||
Example any `json:"example,omitempty"` | ||
} | ||
|
||
jsonSchemaField struct { | ||
Name string `json:"name"` | ||
Description string `json:"description,omitempty"` | ||
Type string `json:"type"` | ||
DataType string `json:"data_type"` | ||
Required bool `json:"required"` | ||
} | ||
) | ||
|
||
func newJSONSchema[T any](td *desc.Table) *jsonSchema[T] { | ||
var fieldTypes []jsonSchemaFieldType | ||
seenFieldTypes := make(map[reflect.Type]struct{}) | ||
|
||
fields := make([]jsonSchemaField, 0, len(td.Columns)) | ||
for _, col := range td.Columns { | ||
fieldName, ok := getJSONTag(col.Table.StructType, col.FieldIndex) | ||
if !ok { | ||
fieldName = col.Name | ||
} | ||
|
||
// Get the field type examples. | ||
if _, seen := seenFieldTypes[col.FieldType]; !seen { | ||
seenFieldTypes[col.FieldType] = struct{}{} | ||
|
||
colValue := reflect.New(col.FieldType).Interface() | ||
if exampler, ok := colValue.(Exampler); ok { | ||
exampleValues := exampler.ListExamples() | ||
fieldTypes = append(fieldTypes, jsonSchemaFieldType{ | ||
Name: col.FieldType.String(), | ||
Example: exampleValues, | ||
}) | ||
} | ||
} | ||
|
||
field := jsonSchemaField{ | ||
// Here we want the json tag name, not the column name. | ||
Name: fieldName, | ||
Description: col.Description, | ||
Type: col.FieldType.String(), | ||
DataType: col.Type.String(), | ||
Required: !col.Nullable, | ||
} | ||
|
||
fields = append(fields, field) | ||
} | ||
|
||
return &jsonSchema[T]{ | ||
Description: td.Description, | ||
Types: fieldTypes, | ||
Fields: fields, | ||
} | ||
} | ||
|
||
func getJSONTag(t reflect.Type, fieldIndex []int) (string, bool) { | ||
if t.Kind() != reflect.Struct { | ||
return "", false | ||
} | ||
|
||
f := t.FieldByIndex(fieldIndex) | ||
jsonTag := f.Tag.Get("json") | ||
if jsonTag == "" { | ||
return "", false | ||
} | ||
|
||
return strings.Split(jsonTag, ",")[0], true | ||
} | ||
|
||
func writeJSON(w http.ResponseWriter, code int, v any) error { | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(code) | ||
return json.NewEncoder(w).Encode(v) | ||
} | ||
|
||
func readJSON(r *http.Request, v any) error { | ||
return json.NewDecoder(r.Body).Decode(v) | ||
} | ||
|
||
func (c *HTTPController[T]) getSchema(s *jsonSchema[T]) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
writeJSON(w, http.StatusOK, s) | ||
}) | ||
} | ||
|
||
type idPayload struct { | ||
ID any `json:"id"` | ||
} | ||
|
||
func toUUIDv4(v [16]uint8) string { | ||
slice := v[:] | ||
// Modify the 7th element to have the form 4xxx | ||
slice[6] = (slice[6] & 0x0f) | 0x40 | ||
// Modify the 9th element to have the form yxxx | ||
slice[8] = (slice[8] & 0x3f) | 0x80 | ||
// Convert to UUIDv4 string | ||
s := fmt.Sprintf("%x-%x-%x-%x-%x", slice[0:4], slice[4:6], slice[6:8], slice[8:10], slice[10:]) | ||
return s | ||
} | ||
|
||
// readPayload reads the request body and returns the entity. | ||
func (c *HTTPController[T]) readPayload(w http.ResponseWriter, r *http.Request) (T, bool) { | ||
var payload T | ||
err := readJSON(r, &payload) | ||
if err != nil { | ||
c.ErrorHandler(w, r, err) | ||
return payload, false | ||
} | ||
|
||
if c.AfterPayloadRead != nil { | ||
return c.AfterPayloadRead(w, r, payload) | ||
} | ||
|
||
return payload, true | ||
} | ||
|
||
// create creates a new entity. | ||
func (c *HTTPController[T]) create(w http.ResponseWriter, r *http.Request) { | ||
entry, ok := c.readPayload(w, r) | ||
if !ok { | ||
return | ||
} | ||
|
||
var id any | ||
err := c.repository.InsertSingle(r.Context(), entry, &id) | ||
if err != nil { | ||
c.ErrorHandler(w, r, err) | ||
return | ||
} | ||
|
||
switch c.primaryKeyType { | ||
case desc.UUID: | ||
// A special case to convert from [16]uint8 to string (uuidv4). We do this in order to not accept a 2nd generic parameter of V. | ||
id = toUUIDv4(id.([16]uint8)) | ||
} | ||
|
||
writeJSON(w, http.StatusCreated, idPayload{ID: id}) | ||
} |
Oops, something went wrong.