summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/spicedb/pkg/datastore
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/datastore')
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go35
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/context.go1
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/counters.go47
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go94
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go992
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/doc.go2
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/errors.go281
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go123
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go65
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go300
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go70
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go109
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go23
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/stats.go31
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/datastore/util.go63
15 files changed, 2236 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go b/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go
new file mode 100644
index 0000000..85dc7b4
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go
@@ -0,0 +1,35 @@
+package datastore
+
+import (
+ "context"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+// RevisionedCaveat is a revisioned version of a caveat definition.
+type RevisionedCaveat = RevisionedDefinition[*core.CaveatDefinition]
+
+// CaveatReader offers read operations for caveats
+type CaveatReader interface {
+ // ReadCaveatByName returns a caveat with the provided name.
+ // It returns an instance of CaveatNotFoundError if not found.
+ ReadCaveatByName(ctx context.Context, name string) (caveat *core.CaveatDefinition, lastWritten Revision, err error)
+
+ // ListAllCaveats returns all caveats stored in the system.
+ ListAllCaveats(ctx context.Context) ([]RevisionedCaveat, error)
+
+ // LookupCaveatsWithNames finds all caveats with the matching names.
+ LookupCaveatsWithNames(ctx context.Context, names []string) ([]RevisionedCaveat, error)
+}
+
+// CaveatStorer offers both read and write operations for Caveats
+type CaveatStorer interface {
+ CaveatReader
+
+ // WriteCaveats stores the provided caveats, and returns the assigned IDs
+ // Each element of the returning slice corresponds by position to the input slice
+ WriteCaveats(context.Context, []*core.CaveatDefinition) error
+
+ // DeleteCaveats deletes the provided caveats by name
+ DeleteCaveats(ctx context.Context, names []string) error
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/context.go b/vendor/github.com/authzed/spicedb/pkg/datastore/context.go
new file mode 100644
index 0000000..1ea451e
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/context.go
@@ -0,0 +1 @@
+package datastore
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go b/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go
new file mode 100644
index 0000000..9b90ea4
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go
@@ -0,0 +1,47 @@
+package datastore
+
+import (
+ "context"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+// RelationshipCounter is a struct that represents a count of relationships that match a filter.
+type RelationshipCounter struct {
+ // Name is the name of the counter.
+ Name string
+
+ // Filter is the filter that the count represents.
+ Filter *core.RelationshipFilter
+
+ // Count is the count of relationships that match the filter.
+ Count int
+
+ // ComputedAtRevision is the revision at which the count was last computed. If NoRevision,
+ // the count has never been computed.
+ ComputedAtRevision Revision
+}
+
+// CounterRegisterer is an interface for registering and unregistering counts.
+type CounterRegisterer interface {
+ // RegisterCounter registers a count with the provided filter. If the counter already exists,
+ // returns an error.
+ RegisterCounter(ctx context.Context, name string, filter *core.RelationshipFilter) error
+
+ // UnregisterCounter unregisters a counter. If the counter did not exist, returns an error.
+ UnregisterCounter(ctx context.Context, name string) error
+
+ // StoreCounterValue stores a count for the counter with the given name, at the given revision.
+ // If the counter does not exist, returns an error.
+ StoreCounterValue(ctx context.Context, name string, value int, computedAtRevision Revision) error
+}
+
+// CounterReader is an interface for reading counts.
+type CounterReader interface {
+ // CountRelationships returns the count of relationships that match the provided counter. If the counter is not
+ // registered, returns an error.
+ CountRelationships(ctx context.Context, name string) (int, error)
+
+ // LookupCounters returns all registered counters.
+ LookupCounters(ctx context.Context) ([]RelationshipCounter, error)
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go b/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go
new file mode 100644
index 0000000..9c4a093
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go
@@ -0,0 +1,94 @@
+package datastore
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ rdsauth "github.com/aws/aws-sdk-go-v2/feature/rds/auth"
+ "golang.org/x/exp/maps"
+
+ log "github.com/authzed/spicedb/internal/logging"
+)
+
+// CredentialsProvider allows datastore credentials to be retrieved dynamically
+type CredentialsProvider interface {
+ // Name returns the name of the provider
+ Name() string
+ // IsCleartextToken returns true if the token returned represents a token (rather than a password) that must be sent in cleartext to the datastore, or false otherwise.
+ // This may be used to configure the datastore options to avoid sending a hash of the token instead of its value.
+ // Note that it is always recommended that communication channel be encrypted.
+ IsCleartextToken() bool
+ // Get returns the username and password to use when connecting to the underlying datastore
+ Get(ctx context.Context, dbEndpoint string, dbUser string) (string, string, error)
+}
+
+var NoCredentialsProvider CredentialsProvider = nil
+
+type credentialsProviderBuilderFunc func(ctx context.Context) (CredentialsProvider, error)
+
+const (
+ // AWSIAMCredentialProvider generates AWS IAM tokens for authenticating with the datastore (i.e. RDS)
+ AWSIAMCredentialProvider = "aws-iam"
+)
+
+var BuilderForCredentialProvider = map[string]credentialsProviderBuilderFunc{
+ AWSIAMCredentialProvider: newAWSIAMCredentialsProvider,
+}
+
+// CredentialsProviderOptions returns the full set of credential provider names, sorted and quoted into a string.
+func CredentialsProviderOptions() string {
+ ids := maps.Keys(BuilderForCredentialProvider)
+ sort.Strings(ids)
+ quoted := make([]string, 0, len(ids))
+ for _, id := range ids {
+ quoted = append(quoted, `"`+id+`"`)
+ }
+ return strings.Join(quoted, ", ")
+}
+
+// NewCredentialsProvider create a new CredentialsProvider for the given name
+// returns an error if no match is found, of if there is a problem creating the given CredentialsProvider
+func NewCredentialsProvider(ctx context.Context, name string) (CredentialsProvider, error) {
+ builder, ok := BuilderForCredentialProvider[name]
+ if !ok {
+ return nil, fmt.Errorf("unknown credentials provider: %s", name)
+ }
+ return builder(ctx)
+}
+
+// AWS IAM provider
+
+func newAWSIAMCredentialsProvider(ctx context.Context) (CredentialsProvider, error) {
+ awsSdkConfig, err := awsconfig.LoadDefaultConfig(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &awsIamCredentialsProvider{awsSdkConfig: awsSdkConfig}, nil
+}
+
+type awsIamCredentialsProvider struct {
+ awsSdkConfig aws.Config
+}
+
+func (d awsIamCredentialsProvider) Name() string {
+ return AWSIAMCredentialProvider
+}
+
+func (d awsIamCredentialsProvider) IsCleartextToken() bool {
+ // The AWS IAM token can be of an arbitrary length and must not be hashed or truncated by the datastore driver
+ // See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
+ return true
+}
+
+func (d awsIamCredentialsProvider) Get(ctx context.Context, dbEndpoint string, dbUser string) (string, string, error) {
+ authToken, err := rdsauth.BuildAuthToken(ctx, dbEndpoint, d.awsSdkConfig.Region, dbUser, d.awsSdkConfig.Credentials)
+ if err != nil {
+ return "", "", err
+ }
+ log.Ctx(ctx).Trace().Str("region", d.awsSdkConfig.Region).Str("endpoint", dbEndpoint).Str("user", dbUser).Msg("successfully retrieved IAM auth token for DB")
+ return dbUser, authToken, nil
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go b/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go
new file mode 100644
index 0000000..24cd25f
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go
@@ -0,0 +1,992 @@
+package datastore
+
+import (
+ "context"
+ "fmt"
+ "iter"
+ "slices"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog"
+ "google.golang.org/protobuf/types/known/structpb"
+
+ "github.com/authzed/spicedb/pkg/tuple"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+
+ "github.com/authzed/spicedb/pkg/datastore/options"
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+var Engines []string
+
+// SortedEngineIDs returns the full set of engine IDs, sorted.
+func SortedEngineIDs() []string {
+ engines := append([]string{}, Engines...)
+ sort.Strings(engines)
+ return engines
+}
+
+// EngineOptions returns the full set of engine IDs, sorted and quoted into a string.
+func EngineOptions() string {
+ ids := SortedEngineIDs()
+ quoted := make([]string, 0, len(ids))
+ for _, id := range ids {
+ quoted = append(quoted, `"`+id+`"`)
+ }
+ return strings.Join(quoted, ", ")
+}
+
+// Ellipsis is a special relation that is assumed to be valid on the right
+// hand side of a tuple.
+const Ellipsis = "..."
+
+// RevisionChanges represents the changes in a single transaction.
+type RevisionChanges struct {
+ Revision Revision
+
+ // RelationshipChanges are any relationships that were changed at this revision.
+ RelationshipChanges []tuple.RelationshipUpdate
+
+ // ChangedDefinitions are any definitions that were added or changed at this revision.
+ ChangedDefinitions []SchemaDefinition
+
+ // DeletedNamespaces are any namespaces that were deleted.
+ DeletedNamespaces []string
+
+ // DeletedCaveats are any caveats that were deleted.
+ DeletedCaveats []string
+
+ // IsCheckpoint, if true, indicates that the datastore has reported all changes
+ // up until and including the Revision and that no additional schema updates can
+ // have occurred before this point.
+ IsCheckpoint bool
+
+ // Metadata is the metadata associated with the revision, if any.
+ Metadata *structpb.Struct
+}
+
+func (rc RevisionChanges) DebugString() string {
+ if rc.IsCheckpoint {
+ return "[checkpoint]"
+ }
+
+ debugString := ""
+
+ for _, relChange := range rc.RelationshipChanges {
+ debugString += relChange.DebugString() + "\n"
+ }
+
+ for _, def := range rc.ChangedDefinitions {
+ debugString += fmt.Sprintf("Definition: %T:%s\n", def, def.GetName())
+ }
+
+ for _, ns := range rc.DeletedNamespaces {
+ debugString += fmt.Sprintf("DeletedNamespace: %s\n", ns)
+ }
+
+ for _, caveat := range rc.DeletedCaveats {
+ debugString += fmt.Sprintf("DeletedCaveat: %s\n", caveat)
+ }
+
+ return debugString
+}
+
+func (rc RevisionChanges) MarshalZerologObject(e *zerolog.Event) {
+ e.Str("revision", rc.Revision.String())
+ e.Bool("is-checkpoint", rc.IsCheckpoint)
+ e.Array("deleted-namespaces", strArray(rc.DeletedNamespaces))
+ e.Array("deleted-caveats", strArray(rc.DeletedCaveats))
+
+ changedNames := make([]string, 0, len(rc.ChangedDefinitions))
+ for _, cd := range rc.ChangedDefinitions {
+ changedNames = append(changedNames, fmt.Sprintf("%T:%s", cd, cd.GetName()))
+ }
+
+ e.Array("changed-definitions", strArray(changedNames))
+ e.Int("num-changed-relationships", len(rc.RelationshipChanges))
+}
+
+// ExpirationFilterOption is the filter option for the expiration field on relationships.
+type ExpirationFilterOption int
+
+const (
+ // ExpirationFilterOptionNone indicates that the expiration filter should not be used:
+ // relationships both with and without expiration will be returned.
+ ExpirationFilterOptionNone ExpirationFilterOption = iota
+
+ // ExpirationFilterOptionHasExpiration indicates that the expiration filter should only
+ // return relationships with an expiration.
+ ExpirationFilterOptionHasExpiration
+
+ // ExpirationFilterOptionNoExpiration indicates that the expiration filter should only
+ // return relationships without an expiration.
+ ExpirationFilterOptionNoExpiration
+)
+
+// CaveatFilterOption is the filter option for the caveat name field on relationships.
+type CaveatFilterOption int
+
+const (
+ // CaveatFilterOptionNone indicates that the caveat filter should not be used:
+ // relationships both with and without caveats will be returned.
+ CaveatFilterOptionNone CaveatFilterOption = iota
+
+ // CaveatFilterOptionHasMatchingCaveat indicates that the caveat filter should only
+ // return relationships with the matching caveat.
+ CaveatFilterOptionHasMatchingCaveat
+
+ // CaveatFilterOptionNoCaveat indicates that the caveat filter should only
+ // return relationships without a caveat.
+ CaveatFilterOptionNoCaveat
+)
+
+// RelationshipsFilter is a filter for relationships.
+type RelationshipsFilter struct {
+ // OptionalResourceType is the namespace/type for the resources to be found.
+ OptionalResourceType string
+
+ // OptionalResourceIds are the IDs of the resources to find. If nil empty, any resource ID will be allowed.
+ // Cannot be used with OptionalResourceIDPrefix.
+ OptionalResourceIds []string
+
+ // OptionalResourceIDPrefix is the prefix to use for resource IDs. If empty, any prefix is allowed.
+ // Cannot be used with OptionalResourceIds.
+ OptionalResourceIDPrefix string
+
+ // OptionalResourceRelation is the relation of the resource to find. If empty, any relation is allowed.
+ OptionalResourceRelation string
+
+ // OptionalSubjectsSelectors is the selectors to use for subjects of the relationship. If nil, all subjects are allowed.
+ // If specified, relationships matching *any* selector will be returned.
+ OptionalSubjectsSelectors []SubjectsSelector
+
+ // OptionalCaveatNameFilter is the filter to use for caveated relationships, filtering by a specific caveat name.
+ // By default, no caveat filtered is done (one direction or the other).
+ OptionalCaveatNameFilter CaveatNameFilter
+
+ // OptionalExpirationOption is the filter to use for relationships with or without an expiration.
+ OptionalExpirationOption ExpirationFilterOption
+}
+
+// Test returns true iff the given relationship is matched by this filter.
+func (rf RelationshipsFilter) Test(relationship tuple.Relationship) bool {
+ if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.Resource.ObjectType {
+ return false
+ }
+
+ if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.Resource.ObjectID) {
+ return false
+ }
+
+ if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.Resource.ObjectID, rf.OptionalResourceIDPrefix) {
+ return false
+ }
+
+ if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.Resource.Relation {
+ return false
+ }
+
+ if len(rf.OptionalSubjectsSelectors) > 0 {
+ for _, selector := range rf.OptionalSubjectsSelectors {
+ if selector.Test(relationship.Subject) {
+ return true
+ }
+ }
+ return false
+ }
+
+ switch rf.OptionalCaveatNameFilter.Option {
+ case CaveatFilterOptionNone:
+ // No caveat filter, so no need to check.
+
+ case CaveatFilterOptionHasMatchingCaveat:
+ if relationship.OptionalCaveat == nil || relationship.OptionalCaveat.CaveatName != rf.OptionalCaveatNameFilter.CaveatName {
+ return false
+ }
+
+ case CaveatFilterOptionNoCaveat:
+ if relationship.OptionalCaveat != nil && relationship.OptionalCaveat.CaveatName != "" {
+ return false
+ }
+ }
+
+ if rf.OptionalExpirationOption == ExpirationFilterOptionHasExpiration && relationship.OptionalExpiration == nil {
+ return false
+ }
+
+ if rf.OptionalExpirationOption == ExpirationFilterOptionNoExpiration && relationship.OptionalExpiration != nil {
+ return false
+ }
+
+ return true
+}
+
+// CaveatNameFilter is a filter for caveat names.
+type CaveatNameFilter struct {
+ // Option is the filter option to use for the caveat name.
+ Option CaveatFilterOption
+
+ // CaveatName is the name of the caveat to filter by. Must be specified if option is
+ // CaveatFilterOptionHasCaveat.
+ CaveatName string
+}
+
+func WithCaveatName(caveatName string) CaveatNameFilter {
+ return CaveatNameFilter{
+ Option: CaveatFilterOptionHasMatchingCaveat,
+ CaveatName: caveatName,
+ }
+}
+
+func WithNoCaveat() CaveatNameFilter {
+ return CaveatNameFilter{
+ Option: CaveatFilterOptionNoCaveat,
+ }
+}
+
+// CoreFilterFromRelationshipFilter constructs a core RelationshipFilter from a V1 RelationshipsFilter.
+func CoreFilterFromRelationshipFilter(filter *v1.RelationshipFilter) *core.RelationshipFilter {
+ return &core.RelationshipFilter{
+ ResourceType: filter.ResourceType,
+ OptionalResourceId: filter.OptionalResourceId,
+ OptionalResourceIdPrefix: filter.OptionalResourceIdPrefix,
+ OptionalRelation: filter.OptionalRelation,
+ OptionalSubjectFilter: coreFilterFromSubjectsFilter(filter.OptionalSubjectFilter),
+ }
+}
+
+func coreFilterFromSubjectsFilter(filter *v1.SubjectFilter) *core.SubjectFilter {
+ if filter == nil {
+ return nil
+ }
+
+ return &core.SubjectFilter{
+ SubjectType: filter.SubjectType,
+ OptionalSubjectId: filter.OptionalSubjectId,
+ OptionalRelation: coreFilterFromSubjectRelationFilter(filter.OptionalRelation),
+ }
+}
+
+func coreFilterFromSubjectRelationFilter(filter *v1.SubjectFilter_RelationFilter) *core.SubjectFilter_RelationFilter {
+ if filter == nil {
+ return nil
+ }
+
+ return &core.SubjectFilter_RelationFilter{
+ Relation: filter.Relation,
+ }
+}
+
+// RelationshipsFilterFromCoreFilter constructs a datastore RelationshipsFilter from a core RelationshipFilter.
+func RelationshipsFilterFromCoreFilter(filter *core.RelationshipFilter) (RelationshipsFilter, error) {
+ var resourceIds []string
+ if filter.OptionalResourceId != "" {
+ resourceIds = []string{filter.OptionalResourceId}
+ }
+
+ var subjectsSelectors []SubjectsSelector
+ if filter.OptionalSubjectFilter != nil {
+ var subjectIds []string
+ if filter.OptionalSubjectFilter.OptionalSubjectId != "" {
+ subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId}
+ }
+
+ relationFilter := SubjectRelationFilter{}
+
+ if filter.OptionalSubjectFilter.OptionalRelation != nil {
+ relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation()
+ if relation != "" {
+ relationFilter = relationFilter.WithNonEllipsisRelation(relation)
+ } else {
+ relationFilter = relationFilter.WithEllipsisRelation()
+ }
+ }
+
+ subjectsSelectors = append(subjectsSelectors, SubjectsSelector{
+ OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType,
+ OptionalSubjectIds: subjectIds,
+ RelationFilter: relationFilter,
+ })
+ }
+
+ if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" {
+ return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix")
+ }
+
+ if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 {
+ return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set")
+ }
+
+ return RelationshipsFilter{
+ OptionalResourceType: filter.ResourceType,
+ OptionalResourceIds: resourceIds,
+ OptionalResourceIDPrefix: filter.OptionalResourceIdPrefix,
+ OptionalResourceRelation: filter.OptionalRelation,
+ OptionalSubjectsSelectors: subjectsSelectors,
+ }, nil
+}
+
+// RelationshipsFilterFromPublicFilter constructs a datastore RelationshipsFilter from an API-defined RelationshipFilter.
+func RelationshipsFilterFromPublicFilter(filter *v1.RelationshipFilter) (RelationshipsFilter, error) {
+ var resourceIds []string
+ if filter.OptionalResourceId != "" {
+ resourceIds = []string{filter.OptionalResourceId}
+ }
+
+ var subjectsSelectors []SubjectsSelector
+ if filter.OptionalSubjectFilter != nil {
+ var subjectIds []string
+ if filter.OptionalSubjectFilter.OptionalSubjectId != "" {
+ subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId}
+ }
+
+ relationFilter := SubjectRelationFilter{}
+
+ if filter.OptionalSubjectFilter.OptionalRelation != nil {
+ relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation()
+ if relation != "" {
+ relationFilter = relationFilter.WithNonEllipsisRelation(relation)
+ } else {
+ relationFilter = relationFilter.WithEllipsisRelation()
+ }
+ }
+
+ subjectsSelectors = append(subjectsSelectors, SubjectsSelector{
+ OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType,
+ OptionalSubjectIds: subjectIds,
+ RelationFilter: relationFilter,
+ })
+ }
+
+ if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" {
+ return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix")
+ }
+
+ if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 {
+ return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set")
+ }
+
+ return RelationshipsFilter{
+ OptionalResourceType: filter.ResourceType,
+ OptionalResourceIds: resourceIds,
+ OptionalResourceIDPrefix: filter.OptionalResourceIdPrefix,
+ OptionalResourceRelation: filter.OptionalRelation,
+ OptionalSubjectsSelectors: subjectsSelectors,
+ }, nil
+}
+
+// SubjectsSelector is a selector for subjects.
+type SubjectsSelector struct {
+ // OptionalSubjectType is the namespace/type for the subjects to be found, if any.
+ OptionalSubjectType string
+
+ // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed.
+ OptionalSubjectIds []string
+
+ // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field
+ // is set, any relation is allowed.
+ RelationFilter SubjectRelationFilter
+}
+
+// Test returns true iff the given subject is matched by this filter.
+func (ss SubjectsSelector) Test(subject tuple.ObjectAndRelation) bool {
+ if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.ObjectType {
+ return false
+ }
+
+ if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectID) {
+ return false
+ }
+
+ if !ss.RelationFilter.IsEmpty() {
+ if ss.RelationFilter.IncludeEllipsisRelation && subject.Relation == tuple.Ellipsis {
+ return true
+ }
+
+ if ss.RelationFilter.NonEllipsisRelation != "" && ss.RelationFilter.NonEllipsisRelation != subject.Relation {
+ return false
+ }
+
+ if ss.RelationFilter.OnlyNonEllipsisRelations && subject.Relation == tuple.Ellipsis {
+ return false
+ }
+ }
+
+ return true
+}
+
+// SubjectRelationFilter is the filter to use for relation(s) of subjects being queried.
+type SubjectRelationFilter struct {
+ // NonEllipsisRelation is the relation of the subject type to find. If empty,
+ // IncludeEllipsisRelation must be true.
+ NonEllipsisRelation string
+
+ // IncludeEllipsisRelation, if true, indicates that the ellipsis relation
+ // should be included as an option.
+ IncludeEllipsisRelation bool
+
+ // OnlyNonEllipsisRelations, if true, indicates that only non-ellipsis relations
+ // should be included.
+ OnlyNonEllipsisRelations bool
+}
+
+// WithOnlyNonEllipsisRelations indicates that only non-ellipsis relations should be included.
+func (sf SubjectRelationFilter) WithOnlyNonEllipsisRelations() SubjectRelationFilter {
+ sf.OnlyNonEllipsisRelations = true
+ sf.NonEllipsisRelation = ""
+ sf.IncludeEllipsisRelation = false
+ return sf
+}
+
+// WithEllipsisRelation indicates that the subject filter should include the ellipsis relation
+// as an option for the subjects' relation.
+func (sf SubjectRelationFilter) WithEllipsisRelation() SubjectRelationFilter {
+ sf.IncludeEllipsisRelation = true
+ sf.OnlyNonEllipsisRelations = false
+ return sf
+}
+
+// WithNonEllipsisRelation indicates that the specified non-ellipsis relation should be included as an
+// option for the subjects' relation.
+func (sf SubjectRelationFilter) WithNonEllipsisRelation(relation string) SubjectRelationFilter {
+ sf.NonEllipsisRelation = relation
+ sf.OnlyNonEllipsisRelations = false
+ return sf
+}
+
+// WithRelation indicates that the specified relation should be included as an
+// option for the subjects' relation.
+func (sf SubjectRelationFilter) WithRelation(relation string) SubjectRelationFilter {
+ if relation == tuple.Ellipsis {
+ return sf.WithEllipsisRelation()
+ }
+ return sf.WithNonEllipsisRelation(relation)
+}
+
+// IsEmpty returns true if the subject relation filter is empty.
+func (sf SubjectRelationFilter) IsEmpty() bool {
+ return !sf.IncludeEllipsisRelation && sf.NonEllipsisRelation == "" && !sf.OnlyNonEllipsisRelations
+}
+
+// SubjectsFilter is a filter for subjects.
+type SubjectsFilter struct {
+ // SubjectType is the namespace/type for the subjects to be found.
+ SubjectType string
+
+ // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed.
+ OptionalSubjectIds []string
+
+ // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field
+ // is set, any relation is allowed.
+ RelationFilter SubjectRelationFilter
+}
+
+func (sf SubjectsFilter) AsSelector() SubjectsSelector {
+ return SubjectsSelector{
+ OptionalSubjectType: sf.SubjectType,
+ OptionalSubjectIds: sf.OptionalSubjectIds,
+ RelationFilter: sf.RelationFilter,
+ }
+}
+
+// SchemaDefinition represents a namespace or caveat definition under a schema.
+type SchemaDefinition interface {
+ GetName() string
+ SizeVT() int
+}
+
+// RevisionedDefinition holds a schema definition and its last updated revision.
+type RevisionedDefinition[T SchemaDefinition] struct {
+ // Definition is the namespace or caveat definition.
+ Definition T
+
+ // LastWrittenRevision is the revision at which the namespace or caveat was last updated.
+ LastWrittenRevision Revision
+}
+
+func (rd RevisionedDefinition[T]) GetLastWrittenRevision() Revision {
+ return rd.LastWrittenRevision
+}
+
+// RevisionedNamespace is a revisioned version of a namespace definition.
+type RevisionedNamespace = RevisionedDefinition[*core.NamespaceDefinition]
+
+// Reader is an interface for reading relationships from the datastore.
+type Reader interface {
+ CaveatReader
+ CounterReader
+
+ // QueryRelationships reads relationships, starting from the resource side.
+ QueryRelationships(
+ ctx context.Context,
+ filter RelationshipsFilter,
+ options ...options.QueryOptionsOption,
+ ) (RelationshipIterator, error)
+
+ // ReverseQueryRelationships reads relationships, starting from the subject.
+ ReverseQueryRelationships(
+ ctx context.Context,
+ subjectsFilter SubjectsFilter,
+ options ...options.ReverseQueryOptionsOption,
+ ) (RelationshipIterator, error)
+
+ // ReadNamespaceByName reads a namespace definition and the revision at which it was created or
+ // last written. It returns an instance of NamespaceNotFoundError if not found.
+ ReadNamespaceByName(ctx context.Context, nsName string) (ns *core.NamespaceDefinition, lastWritten Revision, err error)
+
+ // ListAllNamespaces lists all namespaces defined.
+ ListAllNamespaces(ctx context.Context) ([]RevisionedNamespace, error)
+
+ // LookupNamespacesWithNames finds all namespaces with the matching names.
+ LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]RevisionedNamespace, error)
+}
+
+type ReadWriteTransaction interface {
+ Reader
+ CaveatStorer
+ CounterRegisterer
+
+ // WriteRelationships takes a list of tuple mutations and applies them to the datastore.
+ WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error
+
+ // DeleteRelationships deletes relationships that match the provided filter, with
+ // the optional limit. Returns the number of deleted relationships. If a limit
+ // is provided and reached, the method will return true as the second return value.
+ // Otherwise, the boolean can be ignored.
+ DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter,
+ options ...options.DeleteOptionsOption,
+ ) (uint64, bool, error)
+
+ // WriteNamespaces takes proto namespace definitions and persists them.
+ WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error
+
+ // DeleteNamespaces deletes namespaces including associated relationships.
+ DeleteNamespaces(ctx context.Context, nsNames ...string) error
+
+ // BulkLoad takes a relationship source iterator, and writes all of the
+ // relationships to the backing datastore in an optimized fashion. This
+ // method can and will omit checks and otherwise cut corners in the
+ // interest of performance, and should not be relied upon for OLTP-style
+ // workloads.
+ BulkLoad(ctx context.Context, iter BulkWriteRelationshipSource) (uint64, error)
+}
+
+// TxUserFunc is a type for the function that users supply when they invoke a read-write transaction.
+type TxUserFunc func(context.Context, ReadWriteTransaction) error
+
+// ReadyState represents the ready state of the datastore.
+type ReadyState struct {
+ // Message is a human-readable status message for the current state.
+ Message string
+
+ // IsReady indicates whether the datastore is ready.
+ IsReady bool
+}
+
+// BulkWriteRelationshipSource is an interface for transferring relationships
+// to a backing datastore with a zero-copy methodology.
+type BulkWriteRelationshipSource interface {
+ // Next Returns a pointer to a relation tuple if one is available, or nil if
+ // there are no more or there was an error.
+ //
+ // Note: sources may re-use the same memory address for every tuple, data
+ // may change on every call to next even if the pointer has not changed.
+ Next(ctx context.Context) (*tuple.Relationship, error)
+}
+
+type WatchContent int
+
+const (
+ WatchRelationships WatchContent = 1 << 0
+ WatchSchema WatchContent = 1 << 1
+ WatchCheckpoints WatchContent = 1 << 2
+)
+
+// WatchOptions are options for a Watch call.
+type WatchOptions struct {
+ // Content is the content to watch.
+ Content WatchContent
+
+ // CheckpointInterval is the interval to use for checkpointing in the watch.
+ // If given the zero value, the datastore's default will be used. If smaller
+ // than the datastore's minimum, the minimum will be used.
+ CheckpointInterval time.Duration
+
+ // WatchBufferLength is the length of the buffer for the watch channel. If
+ // given the zero value, the datastore's default will be used.
+ WatchBufferLength uint16
+
+ // WatchBufferWriteTimeout is the timeout for writing to the watch channel.
+ // If given the zero value, the datastore's default will be used.
+ WatchBufferWriteTimeout time.Duration
+
+ // WatchConnectTimeout is the timeout for connecting to the watch channel.
+ // If given the zero value, the datastore's default will be used.
+ // May not be supported by the datastore.
+ WatchConnectTimeout time.Duration
+
+ // MaximumBufferedChangesByteSize is the maximum byte size of the buffered changes struct.
+ // If unspecified, no maximum will be enforced. If the maximum is reached before
+ // the changes can be sent, the watch will be closed with an error.
+ MaximumBufferedChangesByteSize uint64
+
+ // EmissionStrategy defines when are changes streamed to the client. If unspecified, changes will be buffered until
+ // they can be checkpointed, which is the default behavior.
+ EmissionStrategy EmissionStrategy
+}
+
+// EmissionStrategy describes when changes are emitted to the client.
+type EmissionStrategy int
+
+const (
+ // EmitWhenCheckpointedStrategy will buffer changes until a checkpoint is reached. This also means that
+ // changes will be deduplicated and revisions will be sorted before emission as soon as they can be checkpointed.
+ EmitWhenCheckpointedStrategy = iota
+
+ // EmitImmediatelyStrategy emits changes as soon as they are available. This means changes will not be buffered,
+ // and thus will be emitted as soon as they are available, but clients are responsible for buffering, deduplication,
+ // and sorting revisions. In practical terms that can only happens if Checkpoints have been requested, so enabling
+ // EmitImmediatelyStrategy without Checkpoints will return an error.
+ EmitImmediatelyStrategy
+)
+
+// WatchJustRelationships returns watch options for just relationships.
+func WatchJustRelationships() WatchOptions {
+ return WatchOptions{
+ Content: WatchRelationships,
+ }
+}
+
+// WatchJustSchema returns watch options for just schema.
+func WatchJustSchema() WatchOptions {
+ return WatchOptions{
+ Content: WatchSchema,
+ }
+}
+
+// WithCheckpointInterval sets the checkpoint interval on a watch options, returning
+// an updated options struct.
+func (wo WatchOptions) WithCheckpointInterval(interval time.Duration) WatchOptions {
+ return WatchOptions{
+ Content: wo.Content,
+ CheckpointInterval: interval,
+ }
+}
+
+// ReadOnlyDatastore is an interface for reading relationships from the datastore.
+type ReadOnlyDatastore interface {
+ // MetricsID returns an identifier for the datastore for use in metrics.
+ // This identifier is typically the hostname of the datastore (where applicable)
+ // and may not be unique; callers should not rely on uniqueness.
+ MetricsID() (string, error)
+
+ // SnapshotReader creates a read-only handle that reads the datastore at the specified revision.
+ // Any errors establishing the reader will be returned by subsequent calls.
+ SnapshotReader(Revision) Reader
+
+ // OptimizedRevision gets a revision that will likely already be replicated
+ // and will likely be shared amongst many queries.
+ OptimizedRevision(ctx context.Context) (Revision, error)
+
+ // HeadRevision gets a revision that is guaranteed to be at least as fresh as
+ // right now.
+ HeadRevision(ctx context.Context) (Revision, error)
+
+ // CheckRevision checks the specified revision to make sure it's valid and
+ // hasn't been garbage collected.
+ CheckRevision(ctx context.Context, revision Revision) error
+
+ // RevisionFromString will parse the revision text and return the specific type of Revision
+ // used by the specific datastore implementation.
+ RevisionFromString(serialized string) (Revision, error)
+
+ // Watch notifies the caller about changes to the datastore, based on the specified options.
+ //
+ // All events following afterRevision will be sent to the caller.
+ //
+ // Errors returned will fall into a few classes:
+ // - WatchDisconnectedError - the watch has fallen too far behind and has been disconnected.
+ // - WatchCanceledError - the watch was canceled by the caller.
+ // - WatchDisabledError - the watch is disabled by being unsupported by the datastore.
+ // - WatchRetryableError - the watch is retryable, and the caller may retry after some backoff time.
+ // - InvalidRevisionError - the revision specified has passed the datastore's watch history window and
+ // the watch cannot be retried.
+ // - Other errors - the watch should not be retried due to a fatal error.
+ Watch(ctx context.Context, afterRevision Revision, options WatchOptions) (<-chan RevisionChanges, <-chan error)
+
+ // ReadyState returns a state indicating whether the datastore is ready to accept data.
+ // Datastores that require database schema creation will return not-ready until the migrations
+ // have been run to create the necessary tables.
+ ReadyState(ctx context.Context) (ReadyState, error)
+
+ // Features returns an object representing what features this
+ // datastore can support. Can make calls to the database, so should
+ // only be used when a connection is allowed.
+ Features(ctx context.Context) (*Features, error)
+
+ // OfflineFeatures returns an object representing what features this
+ // datastore supports code-wise, without making any calls to the database.
+ OfflineFeatures() (*Features, error)
+
+ // Statistics returns relevant values about the data contained in this cluster.
+ Statistics(ctx context.Context) (Stats, error)
+
+ // Close closes the data store.
+ Close() error
+}
+
+// Datastore represents tuple access for a single namespace.
+type Datastore interface {
+ ReadOnlyDatastore
+
+ // ReadWriteTx starts a read/write transaction, which will be committed if no error is
+ // returned and rolled back if an error is returned.
+ ReadWriteTx(context.Context, TxUserFunc, ...options.RWTOptionsOption) (Revision, error)
+}
+
+// ParsedExplain represents the parsed output of an EXPLAIN statement.
+type ParsedExplain struct {
+ // IndexesUsed is the list of indexes used in the query.
+ IndexesUsed []string
+}
+
+// Explainable is an interface for datastores that support EXPLAIN statements.
+type Explainable interface {
+ // BuildExplainQuery builds an EXPLAIN statement for the given SQL and arguments.
+ BuildExplainQuery(sql string, args []any) (string, []any, error)
+
+ // ParseExplain parses the output of an EXPLAIN statement.
+ ParseExplain(explain string) (ParsedExplain, error)
+
+ // PreExplainStatements returns any statements that should be run before the EXPLAIN statement.
+ PreExplainStatements() []string
+}
+
+// SQLDatastore is an interface for datastores that support SQL-based operations.
+type SQLDatastore interface {
+ Datastore
+ Explainable
+}
+
+// StrictReadDatastore is an interface for datastores that support strict read mode.
+type StrictReadDatastore interface {
+ Datastore
+
+ // IsStrictReadModeEnabled returns whether the datastore is in strict read mode.
+ IsStrictReadModeEnabled() bool
+}
+
+type strArray []string
+
+// MarshalZerologArray implements zerolog array marshalling.
+func (strs strArray) MarshalZerologArray(a *zerolog.Array) {
+ for _, val := range strs {
+ a.Str(val)
+ }
+}
+
+// StartableDatastore is an optional extension to the datastore interface that, when implemented,
+// provides the ability for callers to start background operations on the datastore.
+type StartableDatastore interface {
+ Datastore
+
+ // Start starts any background operations on the datastore. The context provided, if canceled, will
+ // also cancel the background operation(s) on the datastore.
+ Start(ctx context.Context) error
+}
+
+// RepairOperation represents a single kind of repair operation that can be run in a repairable
+// datastore.
+type RepairOperation struct {
+ // Name is the command-line name for the repair operation.
+ Name string
+
+ // Description is the human-readable description for the repair operation.
+ Description string
+}
+
+// RepairableDatastore is an optional extension to the datastore interface that, when implemented,
+// provides the ability for callers to repair the datastore's data in some fashion.
+type RepairableDatastore interface {
+ Datastore
+
+ // Repair runs the repair operation on the datastore.
+ Repair(ctx context.Context, operationName string, outputProgress bool) error
+
+ // RepairOperations returns the available repair operations for the datastore.
+ RepairOperations() []RepairOperation
+}
+
+// UnwrappableDatastore represents a datastore that can be unwrapped into the underlying
+// datastore.
+type UnwrappableDatastore interface {
+ // Unwrap returns the wrapped datastore.
+ Unwrap() Datastore
+}
+
+// UnwrapAs recursively attempts to unwrap the datastore into the specified type
+// In none of the layers of the datastore implement the specified type, nil is returned.
+func UnwrapAs[T any](datastore Datastore) T {
+ var ds T
+ uwds := datastore
+
+ for {
+ var ok bool
+ ds, ok = uwds.(T)
+ if ok {
+ break
+ }
+
+ wds, ok := uwds.(UnwrappableDatastore)
+ if !ok {
+ break
+ }
+
+ uwds = wds.Unwrap()
+ }
+
+ return ds
+}
+
+// FeatureStatus are the possible statuses for a feature in the datastore.
+type FeatureStatus int
+
+const (
+ // FeatureStatusUnknown indicates that the status of the feature is unknown.
+ // This can be returned, for example, when a call is made to OfflineFeatures
+ // but the feature requires a call to the database to determine its status.
+ FeatureStatusUnknown FeatureStatus = iota
+
+ // FeatureSupported indicates that the feature is supported by the datastore.
+ FeatureSupported
+
+ // FeatureUnsupported indicates that the feature is not supported by the datastore.
+ FeatureUnsupported
+)
+
+// Feature represents a capability that a datastore can support, plus an
+// optional message explaining the feature is available (or not).
+type Feature struct {
+ Status FeatureStatus
+ Reason string
+}
+
+// Features holds values that represent what features a database can support.
+type Features struct {
+ // Watch is enabled if the underlying datastore can support the Watch api.
+ Watch Feature
+
+ // ContinuousCheckpointing is enabled if the underlying datastore supports continuous checkpointing
+ // via the Watch API. If not supported, clients of the Watch API may expect checkpoints only when
+ // new transactions are committed.
+ ContinuousCheckpointing Feature
+
+ // WatchEmitsImmediately indicates if the datastore supports the EmitImmediatelyStrategy EmissionStrategy.
+ // If not supported, clients of the Watch API will receive an error when calling Watch API with
+ // EmitImmediatelyStrategy option.
+ WatchEmitsImmediately Feature
+
+ // IntegrityData is enabled if the underlying datastore supports retrieving and storing
+ // integrity information.
+ IntegrityData Feature
+}
+
+// ObjectTypeStat represents statistics for a single object type (namespace).
+type ObjectTypeStat struct {
+ // NumRelations is the number of relations defined in a single object type.
+ NumRelations uint32
+
+ // NumPermissions is the number of permissions defined in a single object type.
+ NumPermissions uint32
+}
+
+// Stats represents statistics for the entire datastore.
+type Stats struct {
+ // UniqueID is a unique string for a single datastore.
+ UniqueID string
+
+ // EstimatedRelationshipCount is a best-guess estimate of the number of relationships
+ // in the datastore. Computing it should use a lightweight method such as reading
+ // table statistics.
+ EstimatedRelationshipCount uint64
+
+ // ObjectTypeStatistics returns a slice element for each object type (namespace)
+ // stored in the datastore.
+ ObjectTypeStatistics []ObjectTypeStat
+}
+
+// RelationshipIterator is an iterator over matched tuples. It is a single use
+// iterator.
+type RelationshipIterator iter.Seq2[tuple.Relationship, error]
+
+func IteratorToSlice(iter RelationshipIterator) ([]tuple.Relationship, error) {
+ results := make([]tuple.Relationship, 0)
+ for rel, err := range iter {
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, rel)
+ }
+ return results, nil
+}
+
+// FirstRelationshipIn returns the first relationship found via the iterator, if any.
+func FirstRelationshipIn(iter RelationshipIterator) (tuple.Relationship, bool, error) {
+ for rel, err := range iter {
+ if err != nil {
+ return tuple.Relationship{}, false, err
+ }
+
+ return rel, true, nil
+ }
+
+ return tuple.Relationship{}, false, nil
+}
+
+// Revision is an interface for a comparable revision type that can be different for
+// each datastore implementation.
+type Revision interface {
+ fmt.Stringer
+
+ // Equal returns whether the revisions should be considered equal.
+ Equal(Revision) bool
+
+ // GreaterThan returns whether the receiver is probably greater than the right hand side.
+ GreaterThan(Revision) bool
+
+ // LessThan returns whether the receiver is probably less than the right hand side.
+ LessThan(Revision) bool
+
+ // ByteSortable returns true if the string representation of the Revision is byte sortable, false otherwise.
+ ByteSortable() bool
+}
+
+type nilRevision struct{}
+
+func (nilRevision) ByteSortable() bool {
+ return false
+}
+
+func (nilRevision) Equal(rhs Revision) bool {
+ return rhs == NoRevision
+}
+
+func (nilRevision) GreaterThan(_ Revision) bool {
+ return false
+}
+
+func (nilRevision) LessThan(_ Revision) bool {
+ return true
+}
+
+func (nilRevision) String() string {
+ return "nil"
+}
+
+// NoRevision is a zero type for the revision that will make changing the
+// revision type in the future a bit easier if necessary. Implementations
+// should use any time they want to signal an empty/error revision.
+var NoRevision Revision = nilRevision{}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go b/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go
new file mode 100644
index 0000000..2ffd9e6
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go
@@ -0,0 +1,2 @@
+// Package datastore contains interfaces and code common to all datastores.
+package datastore
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go b/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go
new file mode 100644
index 0000000..84057d5
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go
@@ -0,0 +1,281 @@
+package datastore
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/rs/zerolog"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+// ErrNotFound is a shared interface for not found errors.
+type ErrNotFound interface {
+ IsNotFoundError() bool
+}
+
+// NamespaceNotFoundError occurs when a namespace was not found.
+type NamespaceNotFoundError struct {
+ error
+ namespaceName string
+}
+
+var _ ErrNotFound = NamespaceNotFoundError{}
+
+func (err NamespaceNotFoundError) IsNotFoundError() bool {
+ return true
+}
+
+// NotFoundNamespaceName is the name of the namespace not found.
+func (err NamespaceNotFoundError) NotFoundNamespaceName() string {
+ return err.namespaceName
+}
+
+// MarshalZerologObject implements zerolog object marshalling.
+func (err NamespaceNotFoundError) MarshalZerologObject(e *zerolog.Event) {
+ e.Err(err.error).Str("namespace", err.namespaceName)
+}
+
+// DetailsMetadata returns the metadata for details for this error.
+func (err NamespaceNotFoundError) DetailsMetadata() map[string]string {
+ return map[string]string{
+ "definition_name": err.namespaceName,
+ }
+}
+
+// WatchDisconnectedError occurs when a watch has fallen too far behind and was forcibly disconnected
+// as a result.
+type WatchDisconnectedError struct{ error }
+
+// WatchCanceledError occurs when a watch was canceled by the caller.
+type WatchCanceledError struct{ error }
+
+// WatchDisabledError occurs when watch is disabled by being unsupported by the datastore.
+type WatchDisabledError struct{ error }
+
+// ReadOnlyError is returned when the operation cannot be completed because the datastore is in
+// read-only mode.
+type ReadOnlyError struct{ error }
+
+// WatchRetryableError is returned when a transient/temporary error occurred in watch and indicates that
+// the caller *may* retry the watch after some backoff time.
+type WatchRetryableError struct{ error }
+
+// InvalidRevisionReason is the reason the revision could not be used.
+type InvalidRevisionReason int
+
+const (
+ // RevisionStale is the reason returned when a revision is outside the window of
+ // validity by being too old.
+ RevisionStale InvalidRevisionReason = iota
+
+ // CouldNotDetermineRevision is the reason returned when a revision for a
+ // request could not be determined.
+ CouldNotDetermineRevision
+)
+
+// InvalidRevisionError occurs when a revision specified to a call was invalid.
+type InvalidRevisionError struct {
+ error
+ revision Revision
+ reason InvalidRevisionReason
+}
+
+// InvalidRevision is the revision that failed.
+func (err InvalidRevisionError) InvalidRevision() Revision {
+ return err.revision
+}
+
+// Reason is the reason the revision failed.
+func (err InvalidRevisionError) Reason() InvalidRevisionReason {
+ return err.reason
+}
+
+// MarshalZerologObject implements zerolog object marshalling.
+func (err InvalidRevisionError) MarshalZerologObject(e *zerolog.Event) {
+ switch err.reason {
+ case RevisionStale:
+ e.Err(err.error).Str("reason", "stale")
+ case CouldNotDetermineRevision:
+ e.Err(err.error).Str("reason", "indeterminate")
+ default:
+ e.Err(err.error).Str("reason", "unknown")
+ }
+}
+
+// NewNamespaceNotFoundErr constructs a new namespace not found error.
+func NewNamespaceNotFoundErr(nsName string) error {
+ return NamespaceNotFoundError{
+ error: fmt.Errorf("object definition `%s` not found", nsName),
+ namespaceName: nsName,
+ }
+}
+
+// NewWatchDisconnectedErr constructs a new watch was disconnected error.
+func NewWatchDisconnectedErr() error {
+ return WatchDisconnectedError{
+ error: fmt.Errorf("watch fell too far behind and was disconnected; consider increasing watch buffer size via the flag --datastore-watch-buffer-length"),
+ }
+}
+
+// NewWatchCanceledErr constructs a new watch was canceled error.
+func NewWatchCanceledErr() error {
+ return WatchCanceledError{
+ error: fmt.Errorf("watch was canceled by the caller"),
+ }
+}
+
+// NewWatchDisabledErr constructs a new watch is disabled error.
+func NewWatchDisabledErr(reason string) error {
+ return WatchDisabledError{
+ error: fmt.Errorf("watch is currently disabled: %s", reason),
+ }
+}
+
+// NewWatchTemporaryErr wraps another error in watch, indicating that the error is likely
+// a temporary condition and clients may consider retrying by calling watch again (vs a fatal error).
+func NewWatchTemporaryErr(wrapped error) error {
+ return WatchRetryableError{
+ error: fmt.Errorf("watch has failed with a temporary condition: %w. please retry the watch", wrapped),
+ }
+}
+
+// NewReadonlyErr constructs an error for when a request has failed because
+// the datastore has been configured to be read-only.
+func NewReadonlyErr() error {
+ return ReadOnlyError{
+ error: fmt.Errorf("datastore is in read-only mode"),
+ }
+}
+
+// NewInvalidRevisionErr constructs a new invalid revision error.
+func NewInvalidRevisionErr(revision Revision, reason InvalidRevisionReason) error {
+ switch reason {
+ case RevisionStale:
+ return InvalidRevisionError{
+ error: fmt.Errorf("revision has expired"),
+ revision: revision,
+ reason: reason,
+ }
+
+ default:
+ return InvalidRevisionError{
+ error: fmt.Errorf("revision was invalid"),
+ revision: revision,
+ reason: reason,
+ }
+ }
+}
+
+// CaveatNameNotFoundError is the error returned when a caveat is not found by its name
+type CaveatNameNotFoundError struct {
+ error
+ name string
+}
+
+var _ ErrNotFound = CaveatNameNotFoundError{}
+
+func (err CaveatNameNotFoundError) IsNotFoundError() bool {
+ return true
+}
+
+// CaveatName returns the name of the caveat that couldn't be found
+func (err CaveatNameNotFoundError) CaveatName() string {
+ return err.name
+}
+
+// NewCaveatNameNotFoundErr constructs a new caveat name not found error.
+func NewCaveatNameNotFoundErr(name string) error {
+ return CaveatNameNotFoundError{
+ error: fmt.Errorf("caveat with name `%s` not found", name),
+ name: name,
+ }
+}
+
+// DetailsMetadata returns the metadata for details for this error.
+func (err CaveatNameNotFoundError) DetailsMetadata() map[string]string {
+ return map[string]string{
+ "caveat_name": err.name,
+ }
+}
+
+// CounterNotRegisteredError indicates that a counter was not registered.
+type CounterNotRegisteredError struct {
+ error
+ counterName string
+}
+
+// NewCounterNotRegisteredErr constructs a new counter not registered error.
+func NewCounterNotRegisteredErr(counterName string) error {
+ return CounterNotRegisteredError{
+ error: fmt.Errorf("counter with name `%s` not found", counterName),
+ counterName: counterName,
+ }
+}
+
+// DetailsMetadata returns the metadata for details for this error.
+func (err CounterNotRegisteredError) DetailsMetadata() map[string]string {
+ return map[string]string{
+ "counter_name": err.counterName,
+ }
+}
+
+// CounterAlreadyRegisteredError indicates that a counter was already registered.
+type CounterAlreadyRegisteredError struct {
+ error
+
+ counterName string
+ filter *core.RelationshipFilter
+}
+
+// NewCounterAlreadyRegisteredErr constructs a new filter not registered error.
+func NewCounterAlreadyRegisteredErr(counterName string, filter *core.RelationshipFilter) error {
+ return CounterAlreadyRegisteredError{
+ error: fmt.Errorf("counter with name `%s` already registered", counterName),
+ counterName: counterName,
+ filter: filter,
+ }
+}
+
+// DetailsMetadata returns the metadata for details for this error.
+func (err CounterAlreadyRegisteredError) DetailsMetadata() map[string]string {
+ subjectType := ""
+ subjectID := ""
+ subjectRelation := ""
+ if err.filter.OptionalSubjectFilter != nil {
+ subjectType = err.filter.OptionalSubjectFilter.SubjectType
+ subjectID = err.filter.OptionalSubjectFilter.OptionalSubjectId
+
+ if err.filter.OptionalSubjectFilter.GetOptionalRelation() != nil {
+ subjectRelation = err.filter.OptionalSubjectFilter.GetOptionalRelation().Relation
+ }
+ }
+
+ return map[string]string{
+ "counter_name": err.counterName,
+ "new_filter_resource_type": err.filter.ResourceType,
+ "new_filter_resource_id": err.filter.OptionalResourceId,
+ "new_filter_resource_id_prefix": err.filter.OptionalResourceIdPrefix,
+ "new_filter_relation": err.filter.OptionalRelation,
+ "new_filter_subject_type": subjectType,
+ "new_filter_subject_id": subjectID,
+ "new_filter_subject_relation": subjectRelation,
+ }
+}
+
+// MaximumChangesSizeExceededError is returned when the maximum size of changes is exceeded.
+type MaximumChangesSizeExceededError struct {
+ error
+ maxSize uint64
+}
+
+// NewMaximumChangesSizeExceededError creates a new MaximumChangesSizeExceededError.
+func NewMaximumChangesSizeExceededError(maxSize uint64) error {
+ return MaximumChangesSizeExceededError{fmt.Errorf("maximum changes byte size of %d exceeded", maxSize), maxSize}
+}
+
+var (
+ ErrClosedIterator = errors.New("unable to iterate: iterator closed")
+ ErrCursorsWithoutSorting = errors.New("cursors are disabled on unsorted results")
+ ErrCursorEmpty = errors.New("cursors are only available after the first result")
+)
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go
new file mode 100644
index 0000000..18477f8
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go
@@ -0,0 +1,123 @@
+package options
+
+import (
+ "context"
+
+ "google.golang.org/protobuf/types/known/structpb"
+
+ "github.com/authzed/spicedb/pkg/datastore/queryshape"
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+ "github.com/authzed/spicedb/pkg/tuple"
+)
+
+//go:generate go run github.com/ecordell/optgen -output zz_generated.query_options.go . QueryOptions ReverseQueryOptions RWTOptions
+//go:generate go run github.com/ecordell/optgen -output zz_generated.delete_options.go . DeleteOptions
+
+// SortOrder is an enum which represents the order in which the caller would like
+// the data returned.
+type SortOrder int8
+
+const (
+ // Unsorted lets the underlying datastore choose the order, or no order at all
+ Unsorted SortOrder = iota
+
+ // ByResource sorts the relationships by the resource component first
+ ByResource
+
+ // BySubject sorts the relationships by the subject component first. Note that
+ // BySubject might be quite a bit slower than ByResource, as relationships are
+ // indexed by resource.
+ BySubject
+)
+
+type Cursor *tuple.Relationship
+
+func ToCursor(r tuple.Relationship) Cursor {
+ spiceerrors.DebugAssert(r.ValidateNotEmpty, "cannot create cursor from empty relationship")
+ return Cursor(&r)
+}
+
+func ToRelationship(c Cursor) *tuple.Relationship {
+ if c == nil {
+ return nil
+ }
+ return (*tuple.Relationship)(c)
+}
+
+// SQLCheckAssertionForTest is a function that can be used to assert a condition on the SQL query string.
+// Assertions will only be run during testing and only apply in datastores that support SQL.
+type SQLCheckAssertionForTest func(sql string)
+
+// SQLIndexInformation holds the expected index names for a SQL query.
+type SQLIndexInformation struct {
+ // ExpectedIndexNames are the name(s) of the index(es) that are expected to be used by this
+ // SQL query.
+ ExpectedIndexNames []string
+}
+
+// SQLExplainCallbackForTest is a callback invoked with the explain plan of the SQL query string.
+type SQLExplainCallbackForTest func(ctx context.Context, sql string, args []any, shape queryshape.Shape, explain string, expectedIndexes SQLIndexInformation) error
+
+// QueryOptions are the options that can affect the results of a normal forward query.
+type QueryOptions struct {
+ Limit *uint64 `debugmap:"visible"`
+ Sort SortOrder `debugmap:"visible"`
+ After Cursor `debugmap:"visible"`
+ SkipCaveats bool `debugmap:"visible"`
+ SkipExpiration bool `debugmap:"visible"`
+
+ // SQLCheckAssertionForTest is a function that can be used to assert a condition on the SQL query string.
+ // For testing and validation only.
+ SQLCheckAssertionForTest SQLCheckAssertionForTest `debugmap:"visible"`
+
+ // SQLExplainCallbackForTest is a callback invoked with the explain plan of the SQL query string.
+ // For testing and validation only.
+ SQLExplainCallbackForTest SQLExplainCallbackForTest `debugmap:"visible"`
+
+ // QueryShape is the marked shape of the query.
+ // For testing and validation only.
+ QueryShape queryshape.Shape `debugmap:"visible"`
+}
+
+// ReverseQueryOptions are the options that can affect the results of a reverse query.
+type ReverseQueryOptions struct {
+ ResRelation *ResourceRelation `debugmap:"visible"`
+
+ LimitForReverse *uint64 `debugmap:"visible"`
+ SortForReverse SortOrder `debugmap:"visible"`
+ AfterForReverse Cursor `debugmap:"visible"`
+
+ // SQLExplainCallbackForTestForReverse is a callback invoked with the explain plan of the SQL query string.
+ // For testing and validation only.
+ SQLExplainCallbackForTestForReverse SQLExplainCallbackForTest `debugmap:"visible"`
+
+ // QueryShapeForReverse is the marked shape of the reverse query.
+ // For testing and validation only.
+ QueryShapeForReverse queryshape.Shape `debugmap:"visible"`
+}
+
+// ResourceRelation combines a resource object type and relation.
+type ResourceRelation struct {
+ Namespace string
+ Relation string
+}
+
+// RWTOptions are options that can affect the way a read-write transaction is
+// executed.
+type RWTOptions struct {
+ DisableRetries bool `debugmap:"visible"`
+ Metadata *structpb.Struct `debugmap:"visible"`
+}
+
+// DeleteOptions are the options that can affect the results of a delete relationships
+// operation.
+type DeleteOptions struct {
+ DeleteLimit *uint64 `debugmap:"visible"`
+}
+
+var (
+ one = uint64(1)
+
+ // LimitOne is a constant *uint64 that can be used with WithLimit requests.
+ LimitOne = &one
+)
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go
new file mode 100644
index 0000000..839c9ad
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go
@@ -0,0 +1,65 @@
+// Code generated by github.com/ecordell/optgen. DO NOT EDIT.
+package options
+
+import (
+ defaults "github.com/creasty/defaults"
+ helpers "github.com/ecordell/optgen/helpers"
+)
+
+type DeleteOptionsOption func(d *DeleteOptions)
+
+// NewDeleteOptionsWithOptions creates a new DeleteOptions with the passed in options set
+func NewDeleteOptionsWithOptions(opts ...DeleteOptionsOption) *DeleteOptions {
+ d := &DeleteOptions{}
+ for _, o := range opts {
+ o(d)
+ }
+ return d
+}
+
+// NewDeleteOptionsWithOptionsAndDefaults creates a new DeleteOptions with the passed in options set starting from the defaults
+func NewDeleteOptionsWithOptionsAndDefaults(opts ...DeleteOptionsOption) *DeleteOptions {
+ d := &DeleteOptions{}
+ defaults.MustSet(d)
+ for _, o := range opts {
+ o(d)
+ }
+ return d
+}
+
+// ToOption returns a new DeleteOptionsOption that sets the values from the passed in DeleteOptions
+func (d *DeleteOptions) ToOption() DeleteOptionsOption {
+ return func(to *DeleteOptions) {
+ to.DeleteLimit = d.DeleteLimit
+ }
+}
+
+// DebugMap returns a map form of DeleteOptions for debugging
+func (d DeleteOptions) DebugMap() map[string]any {
+ debugMap := map[string]any{}
+ debugMap["DeleteLimit"] = helpers.DebugValue(d.DeleteLimit, false)
+ return debugMap
+}
+
+// DeleteOptionsWithOptions configures an existing DeleteOptions with the passed in options set
+func DeleteOptionsWithOptions(d *DeleteOptions, opts ...DeleteOptionsOption) *DeleteOptions {
+ for _, o := range opts {
+ o(d)
+ }
+ return d
+}
+
+// WithOptions configures the receiver DeleteOptions with the passed in options set
+func (d *DeleteOptions) WithOptions(opts ...DeleteOptionsOption) *DeleteOptions {
+ for _, o := range opts {
+ o(d)
+ }
+ return d
+}
+
+// WithDeleteLimit returns an option that can set DeleteLimit on a DeleteOptions
+func WithDeleteLimit(deleteLimit *uint64) DeleteOptionsOption {
+ return func(d *DeleteOptions) {
+ d.DeleteLimit = deleteLimit
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go
new file mode 100644
index 0000000..518db5c
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go
@@ -0,0 +1,300 @@
+// Code generated by github.com/ecordell/optgen. DO NOT EDIT.
+package options
+
+import (
+ queryshape "github.com/authzed/spicedb/pkg/datastore/queryshape"
+ defaults "github.com/creasty/defaults"
+ helpers "github.com/ecordell/optgen/helpers"
+ structpb "google.golang.org/protobuf/types/known/structpb"
+)
+
+type QueryOptionsOption func(q *QueryOptions)
+
+// NewQueryOptionsWithOptions creates a new QueryOptions with the passed in options set
+func NewQueryOptionsWithOptions(opts ...QueryOptionsOption) *QueryOptions {
+ q := &QueryOptions{}
+ for _, o := range opts {
+ o(q)
+ }
+ return q
+}
+
+// NewQueryOptionsWithOptionsAndDefaults creates a new QueryOptions with the passed in options set starting from the defaults
+func NewQueryOptionsWithOptionsAndDefaults(opts ...QueryOptionsOption) *QueryOptions {
+ q := &QueryOptions{}
+ defaults.MustSet(q)
+ for _, o := range opts {
+ o(q)
+ }
+ return q
+}
+
+// ToOption returns a new QueryOptionsOption that sets the values from the passed in QueryOptions
+func (q *QueryOptions) ToOption() QueryOptionsOption {
+ return func(to *QueryOptions) {
+ to.Limit = q.Limit
+ to.Sort = q.Sort
+ to.After = q.After
+ to.SkipCaveats = q.SkipCaveats
+ to.SkipExpiration = q.SkipExpiration
+ to.SQLCheckAssertionForTest = q.SQLCheckAssertionForTest
+ to.SQLExplainCallbackForTest = q.SQLExplainCallbackForTest
+ to.QueryShape = q.QueryShape
+ }
+}
+
+// DebugMap returns a map form of QueryOptions for debugging
+func (q QueryOptions) DebugMap() map[string]any {
+ debugMap := map[string]any{}
+ debugMap["Limit"] = helpers.DebugValue(q.Limit, false)
+ debugMap["Sort"] = helpers.DebugValue(q.Sort, false)
+ debugMap["After"] = helpers.DebugValue(q.After, false)
+ debugMap["SkipCaveats"] = helpers.DebugValue(q.SkipCaveats, false)
+ debugMap["SkipExpiration"] = helpers.DebugValue(q.SkipExpiration, false)
+ debugMap["SQLCheckAssertionForTest"] = helpers.DebugValue(q.SQLCheckAssertionForTest, false)
+ debugMap["SQLExplainCallbackForTest"] = helpers.DebugValue(q.SQLExplainCallbackForTest, false)
+ debugMap["QueryShape"] = helpers.DebugValue(q.QueryShape, false)
+ return debugMap
+}
+
+// QueryOptionsWithOptions configures an existing QueryOptions with the passed in options set
+func QueryOptionsWithOptions(q *QueryOptions, opts ...QueryOptionsOption) *QueryOptions {
+ for _, o := range opts {
+ o(q)
+ }
+ return q
+}
+
+// WithOptions configures the receiver QueryOptions with the passed in options set
+func (q *QueryOptions) WithOptions(opts ...QueryOptionsOption) *QueryOptions {
+ for _, o := range opts {
+ o(q)
+ }
+ return q
+}
+
+// WithLimit returns an option that can set Limit on a QueryOptions
+func WithLimit(limit *uint64) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.Limit = limit
+ }
+}
+
+// WithSort returns an option that can set Sort on a QueryOptions
+func WithSort(sort SortOrder) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.Sort = sort
+ }
+}
+
+// WithAfter returns an option that can set After on a QueryOptions
+func WithAfter(after Cursor) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.After = after
+ }
+}
+
+// WithSkipCaveats returns an option that can set SkipCaveats on a QueryOptions
+func WithSkipCaveats(skipCaveats bool) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.SkipCaveats = skipCaveats
+ }
+}
+
+// WithSkipExpiration returns an option that can set SkipExpiration on a QueryOptions
+func WithSkipExpiration(skipExpiration bool) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.SkipExpiration = skipExpiration
+ }
+}
+
+// WithSQLCheckAssertionForTest returns an option that can set SQLCheckAssertionForTest on a QueryOptions
+func WithSQLCheckAssertionForTest(sQLCheckAssertionForTest SQLCheckAssertionForTest) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.SQLCheckAssertionForTest = sQLCheckAssertionForTest
+ }
+}
+
+// WithSQLExplainCallbackForTest returns an option that can set SQLExplainCallbackForTest on a QueryOptions
+func WithSQLExplainCallbackForTest(sQLExplainCallbackForTest SQLExplainCallbackForTest) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.SQLExplainCallbackForTest = sQLExplainCallbackForTest
+ }
+}
+
+// WithQueryShape returns an option that can set QueryShape on a QueryOptions
+func WithQueryShape(queryShape queryshape.Shape) QueryOptionsOption {
+ return func(q *QueryOptions) {
+ q.QueryShape = queryShape
+ }
+}
+
+type ReverseQueryOptionsOption func(r *ReverseQueryOptions)
+
+// NewReverseQueryOptionsWithOptions creates a new ReverseQueryOptions with the passed in options set
+func NewReverseQueryOptionsWithOptions(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions {
+ r := &ReverseQueryOptions{}
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// NewReverseQueryOptionsWithOptionsAndDefaults creates a new ReverseQueryOptions with the passed in options set starting from the defaults
+func NewReverseQueryOptionsWithOptionsAndDefaults(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions {
+ r := &ReverseQueryOptions{}
+ defaults.MustSet(r)
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// ToOption returns a new ReverseQueryOptionsOption that sets the values from the passed in ReverseQueryOptions
+func (r *ReverseQueryOptions) ToOption() ReverseQueryOptionsOption {
+ return func(to *ReverseQueryOptions) {
+ to.ResRelation = r.ResRelation
+ to.LimitForReverse = r.LimitForReverse
+ to.SortForReverse = r.SortForReverse
+ to.AfterForReverse = r.AfterForReverse
+ to.SQLExplainCallbackForTestForReverse = r.SQLExplainCallbackForTestForReverse
+ to.QueryShapeForReverse = r.QueryShapeForReverse
+ }
+}
+
+// DebugMap returns a map form of ReverseQueryOptions for debugging
+func (r ReverseQueryOptions) DebugMap() map[string]any {
+ debugMap := map[string]any{}
+ debugMap["ResRelation"] = helpers.DebugValue(r.ResRelation, false)
+ debugMap["LimitForReverse"] = helpers.DebugValue(r.LimitForReverse, false)
+ debugMap["SortForReverse"] = helpers.DebugValue(r.SortForReverse, false)
+ debugMap["AfterForReverse"] = helpers.DebugValue(r.AfterForReverse, false)
+ debugMap["SQLExplainCallbackForTestForReverse"] = helpers.DebugValue(r.SQLExplainCallbackForTestForReverse, false)
+ debugMap["QueryShapeForReverse"] = helpers.DebugValue(r.QueryShapeForReverse, false)
+ return debugMap
+}
+
+// ReverseQueryOptionsWithOptions configures an existing ReverseQueryOptions with the passed in options set
+func ReverseQueryOptionsWithOptions(r *ReverseQueryOptions, opts ...ReverseQueryOptionsOption) *ReverseQueryOptions {
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// WithOptions configures the receiver ReverseQueryOptions with the passed in options set
+func (r *ReverseQueryOptions) WithOptions(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions {
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// WithResRelation returns an option that can set ResRelation on a ReverseQueryOptions
+func WithResRelation(resRelation *ResourceRelation) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.ResRelation = resRelation
+ }
+}
+
+// WithLimitForReverse returns an option that can set LimitForReverse on a ReverseQueryOptions
+func WithLimitForReverse(limitForReverse *uint64) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.LimitForReverse = limitForReverse
+ }
+}
+
+// WithSortForReverse returns an option that can set SortForReverse on a ReverseQueryOptions
+func WithSortForReverse(sortForReverse SortOrder) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.SortForReverse = sortForReverse
+ }
+}
+
+// WithAfterForReverse returns an option that can set AfterForReverse on a ReverseQueryOptions
+func WithAfterForReverse(afterForReverse Cursor) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.AfterForReverse = afterForReverse
+ }
+}
+
+// WithSQLExplainCallbackForTestForReverse returns an option that can set SQLExplainCallbackForTestForReverse on a ReverseQueryOptions
+func WithSQLExplainCallbackForTestForReverse(sQLExplainCallbackForTestForReverse SQLExplainCallbackForTest) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.SQLExplainCallbackForTestForReverse = sQLExplainCallbackForTestForReverse
+ }
+}
+
+// WithQueryShapeForReverse returns an option that can set QueryShapeForReverse on a ReverseQueryOptions
+func WithQueryShapeForReverse(queryShapeForReverse queryshape.Shape) ReverseQueryOptionsOption {
+ return func(r *ReverseQueryOptions) {
+ r.QueryShapeForReverse = queryShapeForReverse
+ }
+}
+
+type RWTOptionsOption func(r *RWTOptions)
+
+// NewRWTOptionsWithOptions creates a new RWTOptions with the passed in options set
+func NewRWTOptionsWithOptions(opts ...RWTOptionsOption) *RWTOptions {
+ r := &RWTOptions{}
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// NewRWTOptionsWithOptionsAndDefaults creates a new RWTOptions with the passed in options set starting from the defaults
+func NewRWTOptionsWithOptionsAndDefaults(opts ...RWTOptionsOption) *RWTOptions {
+ r := &RWTOptions{}
+ defaults.MustSet(r)
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// ToOption returns a new RWTOptionsOption that sets the values from the passed in RWTOptions
+func (r *RWTOptions) ToOption() RWTOptionsOption {
+ return func(to *RWTOptions) {
+ to.DisableRetries = r.DisableRetries
+ to.Metadata = r.Metadata
+ }
+}
+
+// DebugMap returns a map form of RWTOptions for debugging
+func (r RWTOptions) DebugMap() map[string]any {
+ debugMap := map[string]any{}
+ debugMap["DisableRetries"] = helpers.DebugValue(r.DisableRetries, false)
+ debugMap["Metadata"] = helpers.DebugValue(r.Metadata, false)
+ return debugMap
+}
+
+// RWTOptionsWithOptions configures an existing RWTOptions with the passed in options set
+func RWTOptionsWithOptions(r *RWTOptions, opts ...RWTOptionsOption) *RWTOptions {
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// WithOptions configures the receiver RWTOptions with the passed in options set
+func (r *RWTOptions) WithOptions(opts ...RWTOptionsOption) *RWTOptions {
+ for _, o := range opts {
+ o(r)
+ }
+ return r
+}
+
+// WithDisableRetries returns an option that can set DisableRetries on a RWTOptions
+func WithDisableRetries(disableRetries bool) RWTOptionsOption {
+ return func(r *RWTOptions) {
+ r.DisableRetries = disableRetries
+ }
+}
+
+// WithMetadata returns an option that can set Metadata on a RWTOptions
+func WithMetadata(metadata *structpb.Struct) RWTOptionsOption {
+ return func(r *RWTOptions) {
+ r.Metadata = metadata
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go b/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go
new file mode 100644
index 0000000..524e52e
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go
@@ -0,0 +1,70 @@
+package pagination
+
+import (
+ "context"
+
+ "github.com/authzed/spicedb/pkg/datastore"
+ "github.com/authzed/spicedb/pkg/datastore/options"
+ "github.com/authzed/spicedb/pkg/datastore/queryshape"
+ "github.com/authzed/spicedb/pkg/tuple"
+)
+
+// NewPaginatedIterator creates an implementation of the datastore.Iterator
+// interface that internally paginates over datastore results.
+func NewPaginatedIterator(
+ ctx context.Context,
+ reader datastore.Reader,
+ filter datastore.RelationshipsFilter,
+ pageSize uint64,
+ order options.SortOrder,
+ startCursor options.Cursor,
+ queryShape queryshape.Shape,
+) (datastore.RelationshipIterator, error) {
+ iter, err := reader.QueryRelationships(
+ ctx,
+ filter,
+ options.WithSort(order),
+ options.WithLimit(&pageSize),
+ options.WithAfter(startCursor),
+ options.WithQueryShape(queryShape),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return func(yield func(tuple.Relationship, error) bool) {
+ cursor := startCursor
+ for {
+ var counter uint64
+ for rel, err := range iter {
+ if !yield(rel, err) {
+ return
+ }
+
+ cursor = options.ToCursor(rel)
+ counter++
+
+ if counter >= pageSize {
+ break
+ }
+ }
+
+ if counter < pageSize {
+ return
+ }
+
+ iter, err = reader.QueryRelationships(
+ ctx,
+ filter,
+ options.WithSort(order),
+ options.WithLimit(&pageSize),
+ options.WithAfter(cursor),
+ options.WithQueryShape(queryShape),
+ )
+ if err != nil {
+ yield(tuple.Relationship{}, err)
+ return
+ }
+ }
+ }, nil
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go b/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go
new file mode 100644
index 0000000..4ff1e0f
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go
@@ -0,0 +1,109 @@
+package queryshape
+
+// Shape represents the different ways a query can be shaped.
+type Shape string
+
+// Symbol guide:
+// *️⃣ - optional
+// ✅ - required
+// 🆔 - has some sort of filter
+// 🅿️ - possibly specified
+
+const (
+ // Unspecified indicates that the shape is not specified.
+ Unspecified Shape = "unspecified"
+
+ // Varying indicates that the shape can vary. This is used
+ // for queries whose shape is not known ahead of time.
+ //
+ // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration
+ Varying = "varying"
+
+ // CheckPermissionSelectDirectSubjects indicates that the query is a permission check
+ // that selects direct subjects.
+ //
+ // The query shape selects a specific relationship based on filling in *all* of it
+ // relationship fields (except the caveat name, context and expiration).
+ //
+ // ✅ resource_type, ✅ resource_id, ✅ resource_relation, ✅ subject_type, ✅ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration
+ CheckPermissionSelectDirectSubjects = "check-permission-select-direct-subjects"
+
+ // CheckPermissionSelectIndirectSubjects indicates that the query is a permission check
+ // that selects indirect subjects.
+ //
+ // The query shape selects a specific relationship based on filling in all fields
+ // on the resource (except the caveat name, context and expiration) and the relation
+ // name. The subject type nor ID is filled in and the optional subject relation is
+ // set to match non-`...`.
+ //
+ // ✅ resource_type, ✅ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, 🆔 subject_relation, *️⃣ caveat, *️⃣ expiration
+ CheckPermissionSelectIndirectSubjects = "check-permission-select-indirect-subjects"
+
+ // AllSubjectsForResources indicates that the query is selecting all subjects for a
+ // given set of resources.
+ //
+ // The query shape selects all subjects for a given set of resources, which are fully
+ // specified by providing the resource type, the resource ID(s) and the relation.
+ //
+ // ✅ resource_type, ✅ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration
+ AllSubjectsForResources = "all-subjects-for-resources"
+
+ // MatchingResourcesForSubject indicates that the query is selecting all resources that
+ // match a given subject.
+ //
+ // The query shape selects all resources that match a given subject, which is specified
+ // by providing the subject type, the subject ID and (optionally) the subject relation.
+ // The resource type and relation are filled in, but the resource ID is never specified.
+ //
+ // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, ✅ subject_type, ✅ subject_id, 🅿️ subject_relation, *️⃣ caveat, *️⃣ expiration
+ MatchingResourcesForSubject = "matching-resources-for-subject"
+
+ // FindResourceOfType indicates that the query is selecting a resource of
+ // a given type.
+ //
+ // The query shape selects a resource of a given type, which is specified by
+ // providing the resource type. The other fields are never specified.
+ //
+ // ✅ resource_type, *️⃣ resource_id, *️⃣ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration
+ FindResourceOfType = "find-resource-of-type"
+
+ // FindSubjectOfType indicates that the query is selecting a subject of
+ // a given type.
+ //
+ // The query shape selects a subject of a given type, which is specified by
+ // providing the subject type. The other fields are never specified.
+ //
+ // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, ✅ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration
+ FindSubjectOfType = "find-subject-of-type"
+
+ // FindResourceOfTypeAndRelation indicates that the query is selecting a single
+ // resource of a given type and relation.
+ //
+ // The query shape selects a resource of a given type and relation, which are
+ // specified by providing the resource type and relation. The other fields are never
+ // specified.
+ //
+ // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration
+ FindResourceOfTypeAndRelation = "find-resource-of-type-and-relation"
+
+ // FindSubjectOfTypeAndRelation indicates that the query is selecting a single
+ // subject of a given type and relation.
+ //
+ // The query shape selects a subject of a given type and relation, which are
+ // specified by providing the subject type and relation. The other fields are never
+ // specified.
+ //
+ // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, ✅ subject_type, *️⃣ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration
+ FindSubjectOfTypeAndRelation = "find-subject-of-type-and-relation"
+
+ // FindResourceRelationForSubjectRelation indicates that the query is selecting a single
+ // relationship type that matches a given relation type, i.e. `user` or
+ // `group#member with somecaveat and expiration`.
+ //
+ // The query shape selects an allowed subject type for a specific relation on a specific
+ // resource type. All fields except resource ID are specified here, with subject ID only
+ // specified if a wildcard.
+ //
+ // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, ✅ subject_type, 🅿️ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration
+ FindResourceRelationForSubjectRelation = "find-resource-relation-for-subject-relation"
+)
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go b/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go
new file mode 100644
index 0000000..2163f07
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go
@@ -0,0 +1,23 @@
+package datastore
+
+type RelationshipQueryOperation int
+
+const (
+ RelationshipQueryNone RelationshipQueryOperation = 0
+ RelationshipQueryOr RelationshipQueryOperation = 1
+ RelationshipQueryAnd RelationshipQueryOperation = 2
+)
+
+type RelationshipsQueryTree struct {
+ op RelationshipQueryOperation
+ filter RelationshipsFilter
+ children []RelationshipsQueryTree
+}
+
+func NewRelationshipQueryTree(filter RelationshipsFilter) RelationshipsQueryTree {
+ return RelationshipsQueryTree{
+ op: RelationshipQueryNone,
+ filter: filter,
+ children: nil,
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go b/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go
new file mode 100644
index 0000000..f98c405
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go
@@ -0,0 +1,31 @@
+package datastore
+
+import (
+ "github.com/authzed/spicedb/pkg/namespace"
+ iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1"
+)
+
+// ComputeObjectTypeStats creates a list of object type stats from an input list of
+// parsed object types.
+func ComputeObjectTypeStats(objTypes []RevisionedNamespace) []ObjectTypeStat {
+ stats := make([]ObjectTypeStat, 0, len(objTypes))
+
+ for _, objType := range objTypes {
+ var relations, permissions uint32
+
+ for _, rel := range objType.Definition.Relation {
+ if namespace.GetRelationKind(rel) == iv1.RelationMetadata_PERMISSION {
+ permissions++
+ } else {
+ relations++
+ }
+ }
+
+ stats = append(stats, ObjectTypeStat{
+ NumRelations: relations,
+ NumPermissions: permissions,
+ })
+ }
+
+ return stats
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/util.go b/vendor/github.com/authzed/spicedb/pkg/datastore/util.go
new file mode 100644
index 0000000..55bb8f2
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/datastore/util.go
@@ -0,0 +1,63 @@
+package datastore
+
+import (
+ "context"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+)
+
+// DefinitionsOf returns just the schema definitions found in the list of revisioned
+// definitions.
+func DefinitionsOf[T SchemaDefinition](revisionedDefinitions []RevisionedDefinition[T]) []T {
+ definitions := make([]T, 0, len(revisionedDefinitions))
+ for _, revDef := range revisionedDefinitions {
+ definitions = append(definitions, revDef.Definition)
+ }
+ return definitions
+}
+
+// DeleteAllData deletes all data from the datastore. Should only be used when explicitly requested.
+// The data is transactionally deleted, which means it may time out.
+func DeleteAllData(ctx context.Context, ds Datastore) error {
+ _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt ReadWriteTransaction) error {
+ nsDefs, err := rwt.ListAllNamespaces(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Delete all relationships.
+ namespaceNames := make([]string, 0, len(nsDefs))
+ for _, nsDef := range nsDefs {
+ _, _, err = rwt.DeleteRelationships(ctx, &v1.RelationshipFilter{
+ ResourceType: nsDef.Definition.Name,
+ })
+ if err != nil {
+ return err
+ }
+ namespaceNames = append(namespaceNames, nsDef.Definition.Name)
+ }
+
+ // Delete all caveats.
+ caveatDefs, err := rwt.ListAllCaveats(ctx)
+ if err != nil {
+ return err
+ }
+
+ caveatNames := make([]string, 0, len(caveatDefs))
+ for _, caveatDef := range caveatDefs {
+ caveatNames = append(caveatNames, caveatDef.Definition.Name)
+ }
+
+ if err := rwt.DeleteCaveats(ctx, caveatNames); err != nil {
+ return err
+ }
+
+ // Delete all namespaces.
+ if err := rwt.DeleteNamespaces(ctx, namespaceNames...); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ return err
+}