diff --git a/jsonb.go b/jsonb.go new file mode 100644 index 0000000..1cf710d --- /dev/null +++ b/jsonb.go @@ -0,0 +1,59 @@ +package dbx + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// JSONB is a type that represents a JSON object stored in the database. It is +// a map[string]any and implements sql.Scanner, driver.Valuer, and Stringer. +type JSONB map[string]any + +// NewJSONB creates a new JSONB value from a map[string]any. +func NewJSONB(data map[string]any) JSONB { + return JSONB(data) +} + +// ToMap converts the JSONB value to a map[string]any. +func (j JSONB) ToMap() map[string]any { + return map[string]any(j) +} + +// Scan implements the sql.Scanner interface. It supports converting from +// string, []byte, or nil into a JSONB value. Attempting to convert from +// any other type will return an error. +func (j *JSONB) Scan(src any) error { + switch v := src.(type) { + case nil: + *j = nil + return nil + case []byte: + return json.Unmarshal(v, j) + case string: + return json.Unmarshal([]byte(v), j) + default: + return fmt.Errorf("Scan: unable to scan type %T into JSONB", v) + } +} + +// Value implements the driver.Valuer interface. It converts the JSONB +// value into a SQL driver value which can be used to directly use the +// JSONB as a parameter to a SQL query. +func (j JSONB) Value() (driver.Value, error) { + if j == nil { + return nil, nil + } + + // Marshal the JSONB to a byte slice + return json.Marshal(j) +} + +func (j JSONB) String() string { + // Marshal the JSONB to a byte slice + bytes, err := json.Marshal(j) + if err != nil { + return "" + } + return string(bytes) +} diff --git a/utility.go b/utility.go new file mode 100644 index 0000000..7f35e99 --- /dev/null +++ b/utility.go @@ -0,0 +1,23 @@ +package dbx + +import ( + "github.com/go-jet/jet/v2/mysql" +) + +// ExprID converts a list of uint64 values to a list of mysql.Expression values +func ExprID(ids []uint64) []mysql.Expression { + expressions := make([]mysql.Expression, len(ids)) + for i, id := range ids { + expressions[i] = mysql.Uint64(id) + } + return expressions +} + +// ExprEnum converts a list of uint8 values to a list of mysql.Expression values +func ExprEnum(enums []uint8) []mysql.Expression { + expressions := make([]mysql.Expression, len(enums)) + for i, enum := range enums { + expressions[i] = mysql.Uint8(enum) + } + return expressions +} diff --git a/uuid.go b/uuid.go new file mode 100644 index 0000000..13386e7 --- /dev/null +++ b/uuid.go @@ -0,0 +1,64 @@ +package dbx + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + + "github.com/segmentio/ksuid" +) + +// UUID is a wrapper around ksuid.KSUID that implements the +// graphql.Unmarshaler and graphql.Marshaler interfaces for use in GraphQL APIs. +// It also provides additional convenience functions such as ParseUUID and NewUUID. +type UUID struct { + ksuid.KSUID +} + +type uuidTransport struct { + UUIDStr string `json:"uuid_str"` +} + +// Generates a new, wrapped KSUID. In the strange case that random bytes can't be read, it will panic. +func NewUUID() UUID { + return UUID{KSUID: ksuid.New()} +} + +// ParseUUID parses a UUID from a string. If the string is not a valid UUID, it will return an error. +func ParseUUID(s string) (UUID, error) { + ksuid, err := ksuid.Parse(s) + if err != nil { + return UUID{}, err + } + return UUID{KSUID: ksuid}, nil +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface +func (u *UUID) UnmarshalGQL(value interface{}) error { + slog.Debug("uuid unmarshaling from gql", "val", value) + str, ok := value.(string) + if !ok { + return fmt.Errorf("GraphQL failed to unmarshal UUID value: %v", value) + } + + if err := u.UnmarshalText([]byte(str)); err != nil { + return err + } + + return nil +} + +// MarshalGQL implements the graphql.Marshaler interface +func (u UUID) MarshalGQL(w io.Writer) { + transport := uuidTransport{UUIDStr: u.String()} + json, err := json.Marshal(transport) + if err != nil { + panic(fmt.Errorf("GraphQL failed to JSON-marshal UUID value: %s", err)) + } + slog.Debug("uuid marshaling to gql", "uuid", u.String(), "json", string(json)) + _, err = w.Write(json) + if err != nil { + panic(fmt.Errorf("GraphQL failed to write UUID value: %s", string(json))) + } +}