mirror of
https://github.com/hanzoai/framework.git
synced 2026-08-06 17:32:06 +00:00
Split from hanzoai/cloud clients/framework, preserving history. The engine no longer depends on hanzoai/cloud at all. Everything the engine took from its host is now an injected seam: - cloud.Service[state] + the package-global 'mounted' become an explicit *Engine value, so a process can run more than one and nothing is reachable through a global. - cek.Open becomes Config.OpenDB. Storage policy is the host's: cloud injects its encrypted-at-rest opener, a standalone app gets plain SQLite. The engine never decides encryption. - principal.Org becomes the Caller value. The engine needs the validated tenant, not the ability to go looking for one, so it takes a value rather than a resolver and cannot be handed a request to inspect. - cloud.Handle HTTP handlers become Engine operations that enforce permissions themselves, so a host cannot forget the gate and does not reimplement it. framework.go -> ops.go. - zip HTTP status mapping becomes Classify + Code, so the engine keeps the knowledge of what a failure means and hosts cannot drift. Operations return Doc (document + its schema) so Wire, the Password redaction choke point, can never be handed a mismatched schema. Adds ops_test.go: permission enforcement moved out of the HTTP handlers, where it was only reachable through a router, so it is proven here at the level every host shares. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
89 lines
3.2 KiB
Go
89 lines
3.2 KiB
Go
package framework
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/hanzoai/doctype"
|
|
)
|
|
|
|
// errors.go is the engine's failure vocabulary.
|
|
//
|
|
// The engine classifies WHAT WENT WRONG; a host maps that to its own status
|
|
// vocabulary (Hanzo Cloud maps it to HTTP). Keeping the classification here is
|
|
// deliberate: "a Link points at a record that does not exist in this org" is
|
|
// engine knowledge, and if each host re-derived it from error strings they
|
|
// would drift. Hosts call Classify and switch on a Code — never on a message.
|
|
|
|
// Sentinel errors. Compare with errors.Is.
|
|
var (
|
|
ErrNotFound = errors.New("framework: not found")
|
|
ErrConflict = errors.New("framework: already exists")
|
|
ErrBadRef = errors.New("framework: referenced record not found in org")
|
|
ErrBadState = errors.New("framework: illegal docstatus transition")
|
|
ErrForbidden = errors.New("framework: forbidden")
|
|
)
|
|
|
|
// HookAbort wraps the error a GATE hook returned. A gate hook
|
|
// (before_insert / before_save / on_submit / on_cancel / on_trash) that returns
|
|
// an error aborts the operation before any state change; wrapping it preserves
|
|
// the hook's message while telling the host this was a refusal, not a fault.
|
|
type HookAbort struct{ Err error }
|
|
|
|
func (h *HookAbort) Error() string { return h.Err.Error() }
|
|
func (h *HookAbort) Unwrap() error { return h.Err }
|
|
|
|
// Code is what an engine failure MEANS.
|
|
type Code int
|
|
|
|
const (
|
|
// CodeInternal is an unexpected fault — the store failed, a marshal failed.
|
|
CodeInternal Code = iota
|
|
// CodeInvalid is a malformed request: a schema violation, a bad field value,
|
|
// an unknown filter field. The caller must change what they sent.
|
|
CodeInvalid
|
|
// CodeForbidden is an authorization refusal: no validated tenant, or a right
|
|
// the caller's roles do not carry.
|
|
CodeForbidden
|
|
// CodeNotFound is a DocType or document that does not exist in this org.
|
|
CodeNotFound
|
|
// CodeConflict is a uniqueness or lifecycle-state collision: the record
|
|
// already exists, or the document is not in a state that permits this.
|
|
CodeConflict
|
|
// CodeRejected is a well-formed request the engine refused on the data's
|
|
// own terms: a dangling Link, or a gate hook that vetoed the operation.
|
|
CodeRejected
|
|
)
|
|
|
|
// Classify maps any engine error to its Code. This is the ONE place that
|
|
// decides what a failure means; a host switches on the result and never
|
|
// re-derives the classification.
|
|
func Classify(err error) Code {
|
|
var abort *HookAbort
|
|
switch {
|
|
case err == nil:
|
|
return CodeInternal // callers must check err != nil first
|
|
case errors.Is(err, ErrForbidden):
|
|
return CodeForbidden
|
|
case errors.Is(err, ErrNotFound):
|
|
return CodeNotFound
|
|
case errors.Is(err, ErrConflict):
|
|
return CodeConflict
|
|
case errors.Is(err, ErrBadState):
|
|
return CodeConflict
|
|
case errors.Is(err, ErrBadRef):
|
|
return CodeRejected
|
|
case errors.As(err, &abort):
|
|
return CodeRejected
|
|
case doctype.IsValidationError(err):
|
|
return CodeInvalid
|
|
default:
|
|
return CodeInternal
|
|
}
|
|
}
|
|
|
|
// IsValidationError reports whether err is a document-schema violation, so an
|
|
// in-process caller can answer "bad request" without string-matching. It is the
|
|
// value layer's predicate, re-exported so a caller that holds only the engine
|
|
// import does not need a second one.
|
|
func IsValidationError(err error) bool { return doctype.IsValidationError(err) }
|