feat: establish Orbit models, migrations and development baseline
This commit is contained in:
16
model/digest.go
Normal file
16
model/digest.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (d Digest) Value() (driver.Value, error) { return d[:], nil }
|
||||
func (d *Digest) Scan(src any) error {
|
||||
b, ok := src.([]byte)
|
||||
if !ok || len(b) != len(d) {
|
||||
return errors.New("digest: expected BINARY(32)")
|
||||
}
|
||||
copy(d[:], b)
|
||||
return nil
|
||||
}
|
||||
65
model/id.go
Normal file
65
model/id.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Package model 提供 Orbit 各模块共用且不绑定存储实现的基础类型。
|
||||
// 服务端模型不直接作为公开 SDK 协议。
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ID 是 128 位随机标识,对外使用小写十六进制,MySQL 使用 BINARY(16)。
|
||||
// 标识只用于定位资源,不能作为访问凭据。
|
||||
type ID string
|
||||
|
||||
func NewID() (ID, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ID(hex.EncodeToString(b[:])), nil
|
||||
}
|
||||
|
||||
func (id ID) Validate() error {
|
||||
if len(id) != 32 {
|
||||
return errors.New("id must contain 32 lowercase hexadecimal characters")
|
||||
}
|
||||
for _, c := range id {
|
||||
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
|
||||
return errors.New("id must contain lowercase hexadecimal characters")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id ID) Value() (driver.Value, error) {
|
||||
if err := id.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hex.DecodeString(string(id))
|
||||
}
|
||||
|
||||
func (id *ID) Scan(src any) error {
|
||||
b, ok := src.([]byte)
|
||||
if !ok || len(b) != 16 {
|
||||
return fmt.Errorf("id: expected BINARY(16), got %T", src)
|
||||
}
|
||||
*id = ID(hex.EncodeToString(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (id *ID) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate := ID(s)
|
||||
if err := candidate.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
*id = candidate
|
||||
return nil
|
||||
}
|
||||
58
model/json.go
Normal file
58
model/json.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// JSON 是经过合法性检查的通用文档。输入内容应使用版本化结构或业务侧引用,
|
||||
// Orbit 不解释行业专用字段。
|
||||
type JSON json.RawMessage
|
||||
|
||||
func (j JSON) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if !json.Valid(j) {
|
||||
return nil, errors.New("invalid JSON document")
|
||||
}
|
||||
return []byte(j), nil
|
||||
}
|
||||
func (j *JSON) Scan(src any) error {
|
||||
if src == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
var b []byte
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
b = v
|
||||
case string:
|
||||
b = []byte(v)
|
||||
default:
|
||||
return fmt.Errorf("JSON: unsupported source %T", src)
|
||||
}
|
||||
if !json.Valid(b) {
|
||||
return errors.New("invalid JSON document")
|
||||
}
|
||||
*j = append((*j)[:0], b...)
|
||||
return nil
|
||||
}
|
||||
func (j JSON) MarshalJSON() ([]byte, error) {
|
||||
if j == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
if !json.Valid(j) {
|
||||
return nil, errors.New("invalid JSON document")
|
||||
}
|
||||
return []byte(j), nil
|
||||
}
|
||||
func (j *JSON) UnmarshalJSON(b []byte) error {
|
||||
if !json.Valid(b) {
|
||||
return errors.New("invalid JSON document")
|
||||
}
|
||||
*j = append((*j)[:0], b...)
|
||||
return nil
|
||||
}
|
||||
51
model/primitives_test.go
Normal file
51
model/primitives_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIDBinaryAndJSONRoundTrip(t *testing.T) {
|
||||
id, err := NewID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
binary, err := id.Value()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var copy ID
|
||||
if err := copy.Scan(binary); err != nil || copy != id {
|
||||
t.Fatalf("binary roundtrip: %s %v", copy, err)
|
||||
}
|
||||
b, err := json.Marshal(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(b, ©); err != nil || copy != id {
|
||||
t.Fatalf("JSON roundtrip: %s %v", copy, err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(`"0123"`), ©); err == nil {
|
||||
t.Fatal("accepted truncated ID")
|
||||
}
|
||||
if _, err := ID("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").Value(); err == nil {
|
||||
t.Fatal("accepted case-folded ID")
|
||||
}
|
||||
if err := copy.Scan([]byte{1}); err == nil {
|
||||
t.Fatal("accepted truncated binary ID")
|
||||
}
|
||||
}
|
||||
func TestJSONScanOwnsBytes(t *testing.T) {
|
||||
src := []byte(`{"ok":true}`)
|
||||
var doc JSON
|
||||
if err := doc.Scan(src); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src[2] = 'x'
|
||||
if string(doc) != `{"ok":true}` {
|
||||
t.Fatal("document aliases driver buffer")
|
||||
}
|
||||
if _, err := JSON(`{broken`).Value(); err == nil {
|
||||
t.Fatal("accepted invalid JSON")
|
||||
}
|
||||
}
|
||||
21
model/record.go
Normal file
21
model/record.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Record 是命名空间内实体的公共记录。Revision 从 1 开始,通过条件更新控制并发。
|
||||
// 时间戳只用于追踪,不代替状态版本和租约校验。
|
||||
type Record struct {
|
||||
// NamespaceID 资源所属命名空间,参与主键、授权与关联校验。
|
||||
NamespaceID ID `db:"namespace_id"`
|
||||
// ID 记录标识;与命名空间一起定位运行资源。
|
||||
ID ID `db:"id"`
|
||||
// Revision 乐观并发版本,从 1 开始;更新时必须核对预期版本。
|
||||
Revision uint64 `db:"revision"`
|
||||
// CreatedAt 创建时间,统一 UTC,数据库精度为微秒。
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
// UpdatedAt 最后写入时间,统一 UTC;不能代替并发版本。
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
// Digest 保存 32 字节 SHA-256 摘要。凭据或执行令牌原文不得进入持久化模型、审计和日志。
|
||||
type Digest [32]byte
|
||||
Reference in New Issue
Block a user