Files
zeekay ab9da76ea8 A Base's records are a typed GraphQL schema, answered through its own router
graph.New(app, router) renders the Base's collections as GraphQL and
answers every field with a request on the Base's own router, made with
the caller's headers, so the list and view rules, tenancy, rate limits
and expand's view checks are the REST path's and there is no second
authorization. zip's engine executes it: a host mounts g.Schema and
g.Resolve with zip.MountGraphFor, so zip is bumped to v1.37.43.

- A type per base and view collection, named from the collection
  (people: BasePerson, person, people, peopleGroupBy, createPerson,
  updatePerson, upsertPerson, deletePerson), with its fields, relations
  as objects, the records pointing at it as connections
  (opportunities_via_company) and its history.
- A filter input per collection whose operators per field type are the
  where clause's: each is one leaf of core.CompileWhere, so and, or,
  not, @me, relative dates, back-relations and inSegment mean what they
  mean there, every value bound.
- orderBy, cursor connections paged through Base's pages, a totalCount
  asked only when selected, and aggregate and GroupBy from the
  aggregate route.
- Bounds: 100 a page, four relations deep refused before any request,
  50 requests a root field, and zip's 10,000 resolutions.

graph_test drives it through zip against a CRM Base: every filter type,
paging both ways, the figures, the writes as REST reads them back, the
refusals in REST's own words (a rule, a superuser-only collection,
another org's token), the budget, and type names on every object.
tests.IAM signs IAM tokens for any test, and apis' helper uses it.
2026-10-19 09:31:00 -07:00

83 lines
2.8 KiB
Go

package graph
import "github.com/hanzoai/base/core"
// composite is the shape of a composite field's value: the object it answers,
// the parts a filter, a sort and an input name, in the order its field names
// them (graph_test holds the two to each other).
type composite struct {
typ string // FullName: BaseFullName, BaseFullNameFilter, …
parts []part
write bool // takes a value in an Input; an actor is Base's to stamp
text bool // contains reads its text parts (the field is a core.TextParts)
}
// part is one named part of a composite value.
type part struct {
name string
kind kind // kText or kNumber; a list part is kJSON (only contains)
list bool // a list of values: answered whole, filtered by contains
elem *object // the object a list part holds, nil for a list of strings
}
// object is an object inside a composite part: a phone, a link.
type object struct {
typ string
fields []string // every one a String
}
var (
phone = &object{typ: "Phone", fields: []string{"number", "countryCode", "callingCode"}}
link = &object{typ: "Link", fields: []string{"url", "label"}}
)
func text(name string) part { return part{name: name, kind: kText} }
func number(name string) part { return part{name: name, kind: kNumber} }
// composites is every composite field type, by the field type it shapes.
var composites = map[string]*composite{
core.FieldTypeCurrency: {typ: "Currency", write: true, parts: []part{
number("amountMicros"), text("currencyCode"),
}},
core.FieldTypeEmails: {typ: "Emails", write: true, text: true, parts: []part{
text("primaryEmail"), {name: "additionalEmails", kind: kJSON, list: true},
}},
core.FieldTypePhones: {typ: "Phones", write: true, text: true, parts: []part{
text("primaryPhoneNumber"), text("primaryPhoneCountryCode"), text("primaryPhoneCallingCode"),
{name: "additionalPhones", kind: kJSON, list: true, elem: phone},
}},
core.FieldTypeLinks: {typ: "Links", write: true, text: true, parts: []part{
text("primaryLinkUrl"), text("primaryLinkLabel"),
{name: "secondaryLinks", kind: kJSON, list: true, elem: link},
}},
core.FieldTypeAddress: {typ: "Address", write: true, text: true, parts: []part{
text("addressStreet1"), text("addressStreet2"), text("addressCity"),
text("addressState"), text("addressPostcode"), text("addressCountry"),
number("addressLat"), number("addressLng"),
}},
core.FieldTypeFullName: {typ: "FullName", write: true, text: true, parts: []part{
text("firstName"), text("lastName"),
}},
core.FieldTypeActor: {typ: "Actor", text: true, parts: []part{
text("source"), text("id"), text("name"),
}},
}
func (c *composite) part(name string) *part {
for i := range c.parts {
if c.parts[i].name == name {
return &c.parts[i]
}
}
return nil
}
func (c *composite) sortable() bool {
for _, p := range c.parts {
if !p.list {
return true
}
}
return false
}