Documentation
¶
Overview ¶
Package namespace names the one database an entity's data lives in.
The fleet stores OLTP data as one SQLite file per entity — an org, a user, a repository — rather than one per tenant, so that a file stays small and several can be opened at once. A namespace is the name of one of those files.
It is a value with constructors and not a string with a validator, because the string form IS a path: it becomes <dir>/<namespace>.db locally and a key prefix in object storage. A name that a validator lets through late is a name that has already been written down somewhere by then. Here there is no route to a Namespace that does not go through a constructor, so the illegal ones do not exist to be caught.
This package is what a namespace IS. Resolving one to an open handle — materialise, open, cache, evict — is hanzoai/orm/db.Namespaces, and it is the only other thing allowed an opinion about namespaces.
Index ¶
- func IsSystem(ctx context.Context) bool
- func Key(ns Namespace, subsystem string) (string, error)
- func NewContext(ctx context.Context, ns Namespace) context.Context
- func Path(dir string, ns Namespace, subsystem string) (string, error)
- func Sanitize(s string) string
- type Group
- type Kind
- type Namespace
- func FromContext(ctx context.Context) (ns Namespace, ok bool)
- func MustOrg(id string) Namespace
- func MustOrgProject(org, project string) Namespace
- func MustRepo(id string) Namespace
- func MustUser(id string) Namespace
- func Of(subject string) (Namespace, error)
- func Org(id string) (Namespace, error)
- func OrgProject(org, project string) (Namespace, error)
- func Parse(s string) (Namespace, error)
- func Repo(id string) (Namespace, error)
- func System() Namespace
- func User(id string) (Namespace, error)
- func (ns Namespace) Group() Group
- func (ns Namespace) ID() string
- func (ns Namespace) IsZero() bool
- func (ns Namespace) Kind() Kind
- func (ns Namespace) MarshalText() ([]byte, error)
- func (ns Namespace) String() string
- func (ns *Namespace) UnmarshalText(b []byte) error
- func (ns Namespace) WithGroup(g Group) Namespace
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsSystem ¶ added in v1.0.1
IsSystem reports whether ctx acts for the system namespace, which spans every entity. Reserve it for work with no requesting user — schedulers, sweeps, migrations — and never derive it from request input.
func Key ¶ added in v1.2.0
Key renders a namespace as the place one subsystem's file for it lives: relative to a data directory on disk, and byte-identically the object key durability ships it to. One rendering, two consumers, so the local file and the remote slot are the same name by construction.
org/{id} → orgs/{id}/{subsystem}.db
org/{id}/{group} → orgs/{id}/projects/{group}/{subsystem}.db
system → orgs/_platform/{subsystem}.db
The subsystem is a separate argument rather than part of the namespace because one store is one subsystem: which subsystem is a property of the registry that opens the file, not of the entity the file belongs to.
Every other kind is an error. User and repo namespaces are real namespaces, but this layout has no place for them, and inventing one silently is how a second convention starts.
func NewContext ¶ added in v1.0.1
NewContext returns a copy of ctx carrying ns. A zero namespace is not carried: it names nothing, so storing it would turn "never set" into "set to nothing" and hide the mistake at the point where it matters.
func Path ¶ added in v1.2.0
Path is Key resolved against a deployment's data directory, in the host's own separator. An empty dir is an error rather than a relative path, because a store that opened relative to whatever the working directory happened to be would be a different file per invocation.
func Sanitize ¶ added in v1.2.0
Sanitize reduces an externally-chosen name — an IAM org, a project — to a lowercase [a-z0-9-] slug, and it is INJECTIVE: two distinct inputs never share an output.
Injectivity is the whole point, because the slug becomes the name of a DATABASE. A lossy fold is not a cosmetic defect there, it is two entities reading and writing one file. The three ways a fold loses information are each closed:
- Invisible runes are REFUSED (→ ""). A name carrying whitespace, a control rune or a zero-width format rune cannot be folded injectively, because strings.TrimSpace and an HTTP header's own OWS trim drop them later regardless of what this function does — "acme " would arrive as "acme". Refusing is the only answer that survives transport. Callers gate on "", so this is fail-secure.
- Case and punctuation are folded, then DISAMBIGUATED: every accepted name that is not already a clean slug maps to its fold plus "-" and the first 16 hex of SHA-256 over the RAW input bytes. Without the suffix, ToLower, every non-[a-z0-9-] rune → "-", and the 32-byte truncation would collapse "Acme"/"acme" and "team.a"/"team-a" onto one name.
- A clean slug that ITSELF looks like this function's own output — <label>-<16 lowercase hex> — is denied the identity fast path and re-suffixed, so an entity that squats on the rendered form of another name cannot alias it. That is the only collision class the fast path could otherwise admit.
It is the identity on a clean, unambiguous, ≤32-byte DNS-1123 label, which is what almost every real name is — so the common case reads as itself on disk.
It does NOT return an error, and that is deliberate: "" is the one refusal, every caller already has to decide what to do about a name it cannot use, and a second failure mode would only be a second thing to forget to check.
Types ¶
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
Group names a set of related entity types kept in a file of their own, so that an entity's data is several small databases and not one that grows without bound: a repository's issues need not be opened to read its settings.
It is a type rather than a string because a group is CODE where an id is DATA. A package declares the groups it owns once —
var Issues = namespace.MustGroup("issues")
— while an id arrives with a request. That is the whole reason the two are built differently: MustGroup fails at init or never, so there is no error to handle at a call site that cannot fail.
The zero Group means no group, i.e. the entity's own data. That is a definition, not a hole: ns.WithGroup(Group{}) is ns.
type Kind ¶
type Kind string
Kind is the sort of entity whose data a namespace holds.
The set is closed and adding to it is a patch to this file, on purpose: which entities own their own data is one fleet-wide fact, and a fact with two homes is a fact that disagrees with itself. It is also the reason Kind cannot be injected — no exported function takes one — so a Kind literal that names nothing can never reach a Namespace.
type Namespace ¶
type Namespace struct {
// contains filtered or unexported fields
}
Namespace names one database: an entity kind, that entity's id, and optionally a group naming which of its related entity types this file holds.
It holds its own canonical string rather than the three parts, so String is free and the map key hashes as one string — Namespaces keys every open handle by this value and looks one up on every request. The parts are derived on the rare path instead, which is the right way round.
The zero Namespace is not a namespace. Go gives every struct a zero value and no encapsulation takes that away, so it is defined rather than left to be discovered: IsZero reports it, String returns "", MarshalText refuses it, and whatever turns a namespace into a path must refuse it too. A zero value that resolved to a file would be one file quietly shared by every caller who forgot to set one — the worst failure this package exists to prevent, so it is the one Go forces us to state explicitly.
func FromContext ¶ added in v1.0.1
FromContext returns the namespace ctx carries. ok is false when nothing set one, which callers should treat as an error rather than a licence to read everything — see IsSystem for work that legitimately spans entities.
func MustOrg ¶
MustOrg, MustUser and MustRepo are for ids fixed in the source — tests, seeds, a constant in a migration. They panic, which is correct for a value that is wrong before the program runs and wrong for anything from a request.
func MustOrgProject ¶ added in v1.2.0
MustOrgProject is OrgProject for names fixed in the source — a test, a seed, a constant in a migration. It panics, which is correct for a value that is wrong before the program runs and wrong for anything from a request.
func Of ¶ added in v1.1.0
Of returns the namespace naming the data that belongs to subject — the key an account holds within its ledger (github.com/hanzoai/account Account.Subject): a bare slug for an org, "<org>/<name>" for a person or a project.
It is a total function of that one string, deliberately. The alternative is a table mapping each account kind to a namespace kind, and a table is somewhere the two vocabularies can drift; deriving both the wallet and the database from the same value means they cannot. If a request bills "hanzo/alice" it reads and writes hanzo/alice's data, by construction rather than by agreement.
The org half is kept rather than collapsing a person to a bare name, so "acme/alice" and "globex/alice" stay different namespaces — and therefore different databases — even where two orgs use the same username.
Validation is inherited: the org must be a legal id and the name a legal group, so a subject with a path separator, a traversal, or an oversized segment is rejected here rather than reaching a filesystem or a key.
func Org ¶
Org, User and Repo name one entity's database.
The id is the entity's STABLE identity, never a human-facing name. A namespace is a path in object storage and a replication prefix; deriving it from something a person can rename — a slug, an email, an owner/name pair — makes a rename into a file move and a broken history. Transferring a repository between orgs is likewise a row somewhere, not a namespace change, which is why a repo is repo/<id> and never org/<org>/repo/<name>.
func OrgProject ¶ added in v1.2.0
OrgProject names the database an org's records live in — or, when project is non-empty, the database that org's records for one project live in.
It is the door for names a PERSON chose. Org and MustOrg take an id that is already legal; OrgProject takes an IAM org and a project as they are written down, folds each through Sanitize — the one injective slugger — and only then builds the namespace. So two distinct orgs can never share a database: a case fold on a case-insensitive filesystem, or a "-"/"." fold, would otherwise collapse them onto one file.
org MUST be a value the caller has already authenticated — a validated claim, or a server-side resolution the caller states as its contract — never a raw request body or header. Sanitize makes a hostile name HARMLESS (it cannot escape its directory) but it cannot make it AUTHORISED, and those are different questions answered in different places.
The project rides in the GROUP slot, which is what a group is for: which of this entity's related types this file holds.
It is stricter than folding alone in exactly one place, and the strictness is the point. Sanitize emits a leading "-" for the two names that fold to nothing printable (one that folds away entirely, such as "!!!", and one literally written "-abc", which takes the identity fast path). A namespace segment must begin with [a-z0-9], so those error here rather than quietly minting a directory named "-<hash>".
func Parse ¶
Parse reads a namespace back from its string form, for the strings that arrive from outside a Go program: a request, a config file, a queue message.
Parse(ns.String()) == ns for every ns a constructor returns, so a namespace survives a round trip instead of degrading into a string with conventions attached to it.
It folds case exactly as the constructors do and repairs nothing else. "Org/Acme" is org/acme because a namespace is a value and "Acme" and "acme" are one org — one value, one spelling, which is canonicalisation. "org//acme" and "org/acme/" are errors, because String never emits them and accepting them would make one namespace have several encodings — which is the aliasing this package removes, arriving through the back door.
func System ¶
func System() Namespace
System is the instance's own namespace: what belongs to the deployment rather than to any org, user or repo — app state, login sources, topics, notices.
It takes no id because there is one instance. Groups keep its contents in separate files (System().WithGroup(Notices)), so "instance-level" costs a namespace and not an exception to the rule that every table lives in one.
func (Namespace) MarshalText ¶
MarshalText and UnmarshalText put every encoder that speaks text — JSON, YAML, a flag, a database column — through Parse and String rather than around them. Without MarshalText, json.Marshal of a Namespace emits {}: the fields that make illegal values unrepresentable also make them invisible to reflection, so this is not a convenience, it is the difference between a namespace and silent data loss.
Marshalling the zero Namespace is an error rather than "": the mistake is at the write, and "" would only fail later at whoever reads it back.
func (*Namespace) UnmarshalText ¶
func (Namespace) WithGroup ¶
WithGroup returns this entity's namespace for one group of its related types.
It REPLACES any group already set instead of nesting, so a namespace is always kind[/id][/group] and there is never a second shape to parse. The zero Namespace absorbs, because there is no entity to name a group of.