mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
The wire carried another vendor's namespace in its error codes (PGRST1xx) and in the media type a caller sends to claim a single row. Both ends are ours, so it said someone else's name for no reason a caller could act on. Two vocabularies meet in a fault and the split is the point: a constraint the STORE refused answers with the store's own SQLSTATE, because every SQL engine already agrees on how to say that; a request the WIRE would not accept answers with a word of ours — bad_query, bad_body, not_one_row, refused, not_found, store_failed — because that is a fact about this API and nobody else gets to name it. Cardinality is asked for with application/vnd.hanzo.object+json. LLM.md described the product as a translation of somebody else's seven services, mapping their names onto ours, which is both wrong (their auth is not Hanzo IAM) and backwards. It says what Base is: one binary, one file, the surfaces it serves, and rules enforced here in Go rather than pushed down. plugins/zap/forward.go still called itself additive to four native message types that plugin.go had already removed — one node, one way in, and the table wire arrives over it like every other route.
144 lines
4.2 KiB
Go
144 lines
4.2 KiB
Go
package validators
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
|
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
|
"github.com/hanzoai/dbx"
|
|
)
|
|
|
|
// UniqueId checks whether a field string id already exists in the specified table.
|
|
//
|
|
// Example:
|
|
//
|
|
// validation.Field(&form.RelId, validation.By(validators.UniqueId(form.app.DB(), "tbl_example"))
|
|
func UniqueId(db dbx.Builder, tableName string) validation.RuleFunc {
|
|
return func(value any) error {
|
|
v, _ := value.(string)
|
|
if v == "" {
|
|
return nil // nothing to check
|
|
}
|
|
|
|
var foundId string
|
|
|
|
err := db.
|
|
Select("id").
|
|
From(tableName).
|
|
Where(dbx.HashExp{"id": v}).
|
|
Limit(1).
|
|
Row(&foundId)
|
|
|
|
if (err != nil && !errors.Is(err, sql.ErrNoRows)) || foundId != "" {
|
|
return validation.NewError("validation_invalid_or_existing_id", "The model id is invalid or already exists.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NormalizeUniqueIndexError attempts to convert a
|
|
// "unique constraint failed" error into a validation.Errors.
|
|
//
|
|
// The provided err is returned as it is without changes if:
|
|
// - err is nil
|
|
// - err is already validation.Errors
|
|
// - err is not "unique constraint failed" error
|
|
func NormalizeUniqueIndexError(err error, tableOrAlias string, fieldNames []string) error {
|
|
if err == nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := err.(validation.Errors); ok {
|
|
return err
|
|
}
|
|
|
|
msg := strings.ToLower(err.Error())
|
|
|
|
if !IsUniqueViolation(err) {
|
|
return err
|
|
}
|
|
|
|
// Postgres names the INDEX rather than the columns
|
|
// ("duplicate key value violates unique constraint \"idx_unique_demo2_title\""),
|
|
// so the field is recovered from the index name instead. Base builds those
|
|
// names out of the column, and matching a whole underscore-delimited token
|
|
// rather than a substring is what keeps a field named "e" from matching
|
|
// every index that happens to contain the letter.
|
|
if !strings.Contains(msg, "unique constraint failed") {
|
|
if idx := uniqueIndexName(msg); idx != "" {
|
|
tokens := map[string]bool{}
|
|
for _, t := range strings.Split(idx, "_") {
|
|
tokens[t] = true
|
|
}
|
|
normalizedErrs := validation.Errors{}
|
|
for _, name := range fieldNames {
|
|
if tokens[strings.ToLower(name)] {
|
|
normalizedErrs[name] = validation.NewError("validation_not_unique", "Value must be unique")
|
|
}
|
|
}
|
|
if len(normalizedErrs) > 0 {
|
|
return normalizedErrs
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// check for unique constraint failure
|
|
if strings.Contains(msg, "unique constraint failed") {
|
|
// note: extra space to unify multi-columns lookup
|
|
msg = strings.ReplaceAll(strings.TrimSpace(msg), ",", " ") + " "
|
|
|
|
normalizedErrs := validation.Errors{}
|
|
|
|
for _, name := range fieldNames {
|
|
// note: extra spaces to exclude table name with suffix matching the current one
|
|
// OR other fields starting with the current field name
|
|
if strings.Contains(msg, strings.ToLower(" "+tableOrAlias+"."+name+" ")) {
|
|
normalizedErrs[name] = validation.NewError("validation_not_unique", "Value must be unique")
|
|
}
|
|
}
|
|
|
|
if len(normalizedErrs) > 0 {
|
|
return normalizedErrs
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// IsUniqueViolation reports whether err is the engine's way of saying a unique
|
|
// index was violated.
|
|
//
|
|
// The two engines this store runs on word it completely differently, and the
|
|
// literal match this used to be ("unique constraint failed") is SQLite's alone —
|
|
// so on Postgres a duplicate never normalized into a field error and surfaced as
|
|
// a 500 instead of a 400. It is also what the table wire reads to answer with
|
|
// SQLSTATE 23505, which is the code REST clients branch on.
|
|
func IsUniqueViolation(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "unique constraint failed") || // sqlite
|
|
strings.Contains(msg, "duplicate key value violates unique constraint") || // postgres
|
|
strings.Contains(msg, "sqlstate 23505") // postgres, when the driver reports the code
|
|
}
|
|
|
|
// uniqueIndexName pulls the quoted index name out of a Postgres duplicate-key
|
|
// message, or returns "" when there is not one to read.
|
|
func uniqueIndexName(msg string) string {
|
|
const marker = `unique constraint "`
|
|
i := strings.Index(msg, marker)
|
|
if i < 0 {
|
|
return ""
|
|
}
|
|
rest := msg[i+len(marker):]
|
|
j := strings.IndexByte(rest, '"')
|
|
if j < 0 {
|
|
return ""
|
|
}
|
|
return rest[:j]
|
|
}
|