diff --git a/apps/server/app/x-pick.go b/apps/server/app/x-pick.go index c2549f8..88b90c7 100644 --- a/apps/server/app/x-pick.go +++ b/apps/server/app/x-pick.go @@ -117,11 +117,16 @@ func (w *fileWork) xPick() { w.P(" ps = append(ps, ", eq, "(k.Get", name.Go(), "()))") } case graph.Edge: + // The Pick is the target's, since it is the target that is + // being found. The getter and the predicate are the edge's: + // the reference message names its field after the edge, and + // so does ent. The two only agree when the edge is named after + // what it points at. name_target := work.Name(p_.Target().Name()) - w.P(" if p, err := ", name_target, "Pick(k.Get", name_target.Go(), "()); err != nil {") + w.P(" if p, err := ", name_target, "Pick(k.Get", name.Go(), "()); err != nil {") w.P(" return nil, ", work.PkgGrpcStatus.Ident("Errorf"), "(", work.PkgGrpcCodes.Ident("InvalidArgument"), ", \"", name_p, ".", name, ": %s\", ", "err)") w.P(" } else {") - w.P(" ps = append(ps, ", x.Ident("Has"+name_target.Go()+"With"), "(p))") + w.P(" ps = append(ps, ", x.Ident("Has"+name.Go()+"With"), "(p))") w.P(" }") default: panic("unknown type of graph prop") diff --git a/internal/apptest/ent/seat.g.go b/internal/apptest/ent/seat.g.go new file mode 100644 index 0000000..8a23412 --- /dev/null +++ b/internal/apptest/ent/seat.g.go @@ -0,0 +1,36 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/seat.proto + +package ent + +import ( + apptest "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + uuid "uuid" +) + +func (e *Seat) Proto() *apptest.Seat { + x := &apptest.Seat{} + x.SetId(e.Id[:]) + if v := e.Edges.Holder; v != nil { + x.SetHolder(v.Proto()) + } else if v := e.HolderId; v != *new(uuid.UUID) { + r := &apptest.User{} + r.SetId(v[:]) + x.SetHolder(r) + } + if v := e.Edges.Org; v != nil { + x.SetOrg(v.Proto()) + } else if v := e.OrgId; v != *new(uuid.UUID) { + r := &apptest.Tenant{} + r.SetId(v[:]) + x.SetOrg(r) + } + if v := e.Edges.Deputy; v != nil { + x.SetDeputy(v.Proto()) + } else if v := e.DeputyId; v != *new(uuid.UUID) { + r := &apptest.User{} + r.SetId(v[:]) + x.SetDeputy(r) + } + return x +} diff --git a/internal/apptest/query.g.go b/internal/apptest/query.g.go index acb0370..c93c620 100644 --- a/internal/apptest/query.g.go +++ b/internal/apptest/query.g.go @@ -338,3 +338,87 @@ func UserByAlias(alias string, tenant *TenantRef) *UserRef { func UserGetByAlias(alias string, tenant *TenantRef) *UserGetRequest { return UserGetRequest_builder{Ref: UserByAlias(alias, tenant)}.Build() } + +func (x *SeatRef) Pick() *SeatGetRequest { + return SeatGetRequest_builder{Ref: x}.Build() +} + +func (x *Seat) Ref() *SeatRef { + if v := x.GetId(); len(v) > 0 { + return SeatById(v) + } + { + v1 := x.GetHolder() + v2 := x.GetOrg() + if v1 != nil && v2 != nil { + return SeatByPlace(v1.Ref(), v2.Ref()) + } + } + { + v1 := x.GetDeputy() + if v1 != nil { + return SeatByDeputy(v1.Ref()) + } + } + + return nil +} + +func (x *Seat) Pick() *SeatGetRequest { + return x.Ref().Pick() +} + +func (x *SeatRef) Picks(v *Seat) bool { + switch x.WhichKey() { + case SeatRef_Id_case: + return bytes.Equal(x.GetId(), v.GetId()) + case SeatRef_Place_case: + x := x.GetPlace() + return (x.GetHolder().Picks(v.GetHolder())) && + (x.GetOrg().Picks(v.GetOrg())) + case SeatRef_Deputy_case: + x := x.GetDeputy() + return (x.GetDeputy().Picks(v.GetDeputy())) + default: + return false + } +} + +func (x *SeatGetRequest) WithSelect(f func(s *SeatSelect)) *SeatGetRequest { + if !x.HasSelect() { + x.SetSelect(&SeatSelect{}) + } + f(x.GetSelect()) + return x +} + +func SeatById(v []byte) *SeatRef { + x := &SeatRef{} + x.SetId(v) + return x +} + +func SeatGetById(v []byte) *SeatGetRequest { + return SeatGetRequest_builder{Ref: SeatById(v)}.Build() +} + +func SeatByPlace(holder *UserRef, org *TenantRef) *SeatRef { + x := &SeatRefByPlace{} + x.SetHolder(holder) + x.SetOrg(org) + return SeatRef_builder{Place: x}.Build() +} + +func SeatGetByPlace(holder *UserRef, org *TenantRef) *SeatGetRequest { + return SeatGetRequest_builder{Ref: SeatByPlace(holder, org)}.Build() +} + +func SeatByDeputy(deputy *UserRef) *SeatRef { + x := &SeatRefByDeputy{} + x.SetDeputy(deputy) + return SeatRef_builder{Deputy: x}.Build() +} + +func SeatGetByDeputy(deputy *UserRef) *SeatGetRequest { + return SeatGetRequest_builder{Ref: SeatByDeputy(deputy)}.Build() +} diff --git a/internal/apptest/schema/seat.go b/internal/apptest/schema/seat.go new file mode 100644 index 0000000..78c3181 --- /dev/null +++ b/internal/apptest/schema/seat.go @@ -0,0 +1,60 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/seat.proto + +package schema + +import ( + ent "github.com/protobuf-orm/ent" + entsql "github.com/protobuf-orm/ent/dialect/entsql" + schema "github.com/protobuf-orm/ent/schema" + edge "github.com/protobuf-orm/ent/schema/edge" + field "github.com/protobuf-orm/ent/schema/field" + index "github.com/protobuf-orm/ent/schema/index" +) + +type Seat struct { + ent.Schema +} + +func (Seat) Fields() []ent.Field { + return []ent.Field{ + field.Uuid("id"). + Unique(). + Immutable(), + field.Uuid("holder_id"), + field.Uuid("org_id"), + field.Uuid("deputy_id"). + Optional(), + } +} + +func (Seat) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("holder", User.Type). + Unique(). + Field("holder_id"). + Required(), + edge.To("org", Tenant.Type). + Unique(). + Field("org_id"). + Required(), + edge.To("deputy", User.Type). + Unique(). + Field("deputy_id"), + } +} + +func (Seat) Indexes() []ent.Index { + return []ent.Index{ + index.Edges("holder", "org"). + Unique(), + index.Edges("deputy"). + Unique(), + } +} + +func (Seat) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "seat"}, + } +} diff --git a/internal/apptest/server/bare/seat.g.go b/internal/apptest/server/bare/seat.g.go new file mode 100644 index 0000000..6394d8e --- /dev/null +++ b/internal/apptest/server/bare/seat.g.go @@ -0,0 +1,509 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/seat.proto + +package bare + +import ( + context "context" + errors "errors" + patchpb "github.com/lesomnus/protobuf-patch/patchpb" + ent1 "github.com/protobuf-orm/ent" + sqlgraph "github.com/protobuf-orm/ent/dialect/sql/sqlgraph" + graph "github.com/protobuf-orm/protobuf-orm/graph" + ormpatch "github.com/protobuf-orm/protobuf-orm/ormpatch" + apptest "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + ent "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent" + predicate "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/predicate" + seat "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/seat" + entpatch "github.com/protobuf-orm/protoc-gen-orm-ent/runtime/entpatch" + entuuid "github.com/protobuf-orm/protoc-gen-orm-ent/runtime/entuuid" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + uuid "uuid" +) + +type SeatServiceServer struct { + Store + + apptest.UnimplementedSeatServiceServer +} + +// NewSeatServiceServer answers with a server that runs its queries with `db`. +// +// It takes the options of [Server] so that what is built here can be told +// where to report its writes and what it may see. Built without them, it +// reports nowhere and sees everything. +func NewSeatServiceServer(db *ent.Client, opts ...Option) apptest.SeatServiceServer { + s := Server{Store: Store{Db: db}} + for _, opt := range opts { + opt(&s) + } + return SeatServiceServer{Store: s.Store} +} + +// SeatNarrow answers with `p` and everything else that narrows a +// read of a Seat, which is whatever `scope` says. +// +// Every read this package makes goes through it, and a read written by +// hand should too -- a List is the one read nothing generates, and so the +// one that would otherwise answer with rows nobody should be given. +func SeatNarrow(ctx context.Context, scope Scope, p predicate.Seat) (predicate.Seat, error) { + ps := make([]predicate.Seat, 0, 2) + if p != nil { + ps = append(ps, p) + } + if scope != nil { + q, err := scope.SeatScope(ctx) + if err != nil { + return nil, err + } + if q != nil { + ps = append(ps, q) + } + } + + switch len(ps) { + case 0: + return nil, nil + case 1: + return ps[0], nil + default: + return seat.And(ps...), nil + } +} + +// narrow is [SeatNarrow] with this server's own scope. +func (s SeatServiceServer) narrow(ctx context.Context, p predicate.Seat) (predicate.Seat, error) { + return SeatNarrow(ctx, s.Scope, p) +} + +func (s SeatServiceServer) Add(ctx context.Context, req *apptest.SeatAddRequest) (*apptest.Seat, error) { + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, s.Rec != nil) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + ds := make([]func(v *apptest.Seat), 0, 3) + q := st.Db.Seat.Create() + var k uuid.UUID + if req.HasId() { + if v, err := entuuid.FromBytes(req.GetId()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + k = v + } + } + if v, err := mint(ctx, s.Mint, "apptest.Seat", k, req.HasId()); err != nil { + return nil, err + } else { + q.SetId(v) + } + if k, err := UserGetKey(ctx, st.Db, req.GetHolder()); err != nil { + return nil, err + } else { + q.SetHolderId(k) + ds = append(ds, func(v *apptest.Seat) { + v.SetHolder(apptest.User_builder{Id: k[:]}.Build()) + }) + } + if k, err := TenantGetKey(ctx, st.Db, req.GetOrg()); err != nil { + return nil, err + } else { + q.SetOrgId(k) + ds = append(ds, func(v *apptest.Seat) { + v.SetOrg(apptest.Tenant_builder{Id: k[:]}.Build()) + }) + } + if req.HasDeputy() { + if k, err := UserGetKey(ctx, st.Db, req.GetDeputy()); err != nil { + return nil, err + } else { + q.SetDeputyId(k) + ds = append(ds, func(v *apptest.Seat) { + v.SetDeputy(apptest.User_builder{Id: k[:]}.Build()) + }) + } + } + + u, err := q.Save(ctx) + if err != nil { + if err, ok := err.(*ent.ConstraintError); ok { + if sqlgraph.IsUniqueConstraintError(err) { + return nil, status.Error(codes.AlreadyExists, "Seat already exists") + } + if sqlgraph.IsForeignKeyConstraintError(err) { + return nil, status.Error(codes.NotFound, "Seat: referenced entity not found") + } + } + return nil, err + } + + if err := record(ctx, s.Rec, st.Db, Change{ + By: apptest.SeatService_Add_FullMethodName, + Key: u.Id, + }); err != nil { + return nil, err + } + if err := tx.Commit(); err != nil { + return nil, err + } + + v := u.Proto() + for _, d := range ds { + d(v) + } + return v, nil +} + +func (s SeatServiceServer) Get(ctx context.Context, req *apptest.SeatGetRequest) (*apptest.Seat, error) { + p, err := SeatPick(req.GetRef()) + if err != nil { + return nil, err + } + p, err = s.narrow(ctx, p) + if err != nil { + return nil, err + } + + q := s.Db.Seat.Query().Where(p) + SeatSelectInit(q, req.GetSelect()) + + v, err := q.Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, status.Error(codes.NotFound, "Seat not found") + } + return nil, err + } + return v.Proto(), nil +} + +func selectSeatKey(q *ent.SeatQuery) { + q.Select(seat.FieldId) +} + +func SeatSelectedFields(m *apptest.SeatSelect) []string { + if m.GetAll() { + return seat.Columns + } + + vs := make([]string, 0, len(seat.Columns)) + { + vs = append(vs, seat.FieldId) + } + + return vs +} + +func SeatSelect(q *ent.SeatQuery, m *apptest.SeatSelect) { + if !m.GetAll() { + fields := SeatSelectedFields(m) + q.Select(fields...) + } + if m.HasHolder() { + q.WithHolder(func(q *ent.UserQuery) { + UserSelect(q, m.GetHolder()) + }) + } + if m.HasOrg() { + q.WithOrg(func(q *ent.TenantQuery) { + TenantSelect(q, m.GetOrg()) + }) + } + if m.HasDeputy() { + q.WithDeputy(func(q *ent.UserQuery) { + UserSelect(q, m.GetDeputy()) + }) + } +} + +func SeatSelectInit(q *ent.SeatQuery, m *apptest.SeatSelect) { + if m != nil { + SeatSelect(q, m) + } else { + q.WithHolder(selectUserKey) + q.WithOrg(selectTenantKey) + q.WithDeputy(selectUserKey) + } +} + +func (s SeatServiceServer) Patch(ctx context.Context, req *apptest.SeatPatchRequest) (*apptest.Seat, error) { + doc, err := ormpatch.FromPatchRequest(seatOrmEntity, req.ProtoReflect(), func(ed graph.Edge, ref protoreflect.Message) (protoreflect.Value, error) { + switch ed.Number() { + case 2: + k, err := UserGetKey(ctx, s.Db, ref.Interface().(*apptest.UserRef)) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(k[:]), nil + case 3: + k, err := TenantGetKey(ctx, s.Db, ref.Interface().(*apptest.TenantRef)) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(k[:]), nil + case 4: + k, err := UserGetKey(ctx, s.Db, ref.Interface().(*apptest.UserRef)) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(k[:]), nil + } + return protoreflect.Value{}, status.Errorf(codes.Internal, "no key resolver for edge: %s", ed.Name()) + }) + if err != nil { + if _, ok := status.FromError(err); ok { + return nil, err + } + if errors.Is(err, ormpatch.ErrRequestLayout) { + return nil, status.Errorf(codes.Internal, "%s", err) + } + if errors.Is(err, ormpatch.ErrUnsupported) { + return nil, status.Errorf(codes.Unimplemented, "%s", err) + } + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + + return s.apply(ctx, req.GetRef(), doc, apptest.SeatService_Patch_FullMethodName) +} + +func SeatGetKey(ctx context.Context, db *ent.Client, ref *apptest.SeatRef) (uuid.UUID, error) { + var z uuid.UUID + if ref.HasId() { + if v, err := entuuid.FromBytes(ref.GetId()); err != nil { + return z, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + return v, nil + } + } + + p, err := SeatPick(ref) + if err != nil { + return z, err + } + + v, err := db.Seat.Query().Where(p).OnlyId(ctx) + if err != nil { + if ent.IsNotFound(err) { + return z, status.Error(codes.NotFound, "Seat not found") + } + return z, err + } + + return v, nil +} + +var seatOrmEntity = ormpatch.MustEntityOf(apptest.File_apptest_seat_proto, "Seat") + +var seatPatchColumns = entpatch.Columns{ + 1: seat.FieldId, 2: seat.HolderColumn, 3: seat.OrgColumn, 4: seat.DeputyColumn} + +func (s SeatServiceServer) Apply(ctx context.Context, req *apptest.SeatApplyRequest) (*apptest.Seat, error) { + if !req.HasPatch() { + return nil, status.Errorf(codes.InvalidArgument, "%s", ormpatch.ErrNoPatch) + } + return s.apply(ctx, req.GetRef(), req.GetPatch(), apptest.SeatService_Apply_FullMethodName) +} + +func (s SeatServiceServer) apply(ctx context.Context, ref *apptest.SeatRef, doc *patchpb.Patch, by string) (*apptest.Seat, error) { + plan := &ormpatch.Plan{Entity: seatOrmEntity} + if doc != nil { + v, err := ormpatch.Compile(seatOrmEntity, doc) + if err != nil { + if errors.Is(err, ormpatch.ErrUnsupported) { + return nil, status.Errorf(codes.Unimplemented, "%s", err) + } + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + plan = v + } + + pred, mod, err := entpatch.Build(plan, seatPatchColumns, s.Db.Dialect()) + if err != nil { + if errors.Is(err, entpatch.ErrValue) { + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + return nil, status.Errorf(codes.Internal, "%s", err) + } + + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, true) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + k, err := SeatGetKey(ctx, st.Db, ref) + if err != nil { + return nil, err + } + at := &apptest.SeatRef{} + at.SetId(k[:]) + p, err := s.narrow(ctx, seat.IdEQ(k)) + if err != nil { + return nil, err + } + + if mod == nil { + q := st.Db.Seat.Query().Where(p) + if pred != nil { + q.Where(predicate.Seat(pred)) + } + if ok, err := q.Exist(ctx); err != nil { + return nil, err + } else if !ok { + return nil, func() error { + if ok, err := st.Db.Seat.Query().Where(p).Exist(ctx); err != nil { + return err + } else if !ok { + return status.Error(codes.NotFound, "Seat not found") + } + return status.Error(codes.FailedPrecondition, "a test in the patch did not hold") + }() + } + } else { + q := st.Db.Seat.Update().Where(p) + if pred != nil { + q.Where(predicate.Seat(pred)) + } + q.Modify(mod) + if n, err := q.Save(ctx); err != nil { + if err, ok := err.(*ent.ConstraintError); ok { + if sqlgraph.IsUniqueConstraintError(err) { + return nil, status.Error(codes.AlreadyExists, "Seat already exists") + } + if sqlgraph.IsForeignKeyConstraintError(err) { + return nil, status.Error(codes.NotFound, "Seat: referenced entity not found") + } + } + return nil, err + } else if n == 0 { + return nil, func() error { + if ok, err := st.Db.Seat.Query().Where(p).Exist(ctx); err != nil { + return err + } else if !ok { + return status.Error(codes.NotFound, "Seat not found") + } + return status.Error(codes.FailedPrecondition, "a test in the patch did not hold") + }() + } + } + + if mod != nil { + if err := record(ctx, s.Rec, st.Db, Change{ + By: by, + Key: k, + Patch: doc, + }); err != nil { + return nil, err + } + } + + out, err := st.Get(ctx, at.Pick()) + if err != nil { + return nil, err + } + if err := tx.Commit(); err != nil { + return nil, err + } + return out, nil +} + +func (s SeatServiceServer) Erase(ctx context.Context, req *apptest.SeatRef) (*apptest.SeatEraseResponse, error) { + p, err := SeatPick(req) + if err != nil { + return nil, err + } + p, err = s.narrow(ctx, p) + if err != nil { + return nil, err + } + + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, s.Rec != nil) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + var k any + if s.Rec != nil { + v, err := st.Db.Seat.Query().Where(p).OnlyId(ctx) + if err != nil { + if ent.IsNotFound(err) { + return &apptest.SeatEraseResponse{}, nil + } + return nil, err + } + + k = v + p = seat.And(p, seat.IdEQ(v)) + } + + n, err := st.Db.Seat.Delete().Where(p).Exec(ctx) + if err != nil { + return nil, err + } + if n > 0 { + if err := record(ctx, s.Rec, st.Db, Change{ + By: apptest.SeatService_Erase_FullMethodName, + Key: k, + }); err != nil { + return nil, err + } + } + if err := tx.Commit(); err != nil { + return nil, err + } + res := &apptest.SeatEraseResponse{} + res.SetErased(n > 0) + + return res, nil +} + +func SeatPick(req *apptest.SeatRef) (predicate.Seat, error) { + switch req.WhichKey() { + case apptest.SeatRef_Id_case: + if v, err := entuuid.FromBytes(req.GetId()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + return seat.IdEQ(v), nil + } + case apptest.SeatRef_Place_case: + k := req.GetPlace() + ps := make([]predicate.Seat, 0, 2) + if p, err := UserPick(k.GetHolder()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "place.holder: %s", err) + } else { + ps = append(ps, seat.HasHolderWith(p)) + } + if p, err := TenantPick(k.GetOrg()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "place.org: %s", err) + } else { + ps = append(ps, seat.HasOrgWith(p)) + } + return seat.And(ps...), nil + case apptest.SeatRef_Deputy_case: + k := req.GetDeputy() + ps := make([]predicate.Seat, 0, 1) + if p, err := UserPick(k.GetDeputy()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "deputy.deputy: %s", err) + } else { + ps = append(ps, seat.HasDeputyWith(p)) + } + return seat.And(ps...), nil + case apptest.SeatRef_Key_not_set_case: + return nil, status.Errorf(codes.InvalidArgument, "key not set: Seat") + default: + return nil, status.Errorf(codes.Unimplemented, "unknown type of key: %s", req.WhichKey()) + } +} diff --git a/internal/apptest/server/bare/store.g.go b/internal/apptest/server/bare/store.g.go index 2c3ed1f..55a8e97 100644 --- a/internal/apptest/server/bare/store.g.go +++ b/internal/apptest/server/bare/store.g.go @@ -14,6 +14,7 @@ import ( messagefield "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/messagefield" note "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/note" predicate "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/predicate" + seat "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/seat" tenant "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/tenant" user "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/user" valuefield "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/valuefield" @@ -319,6 +320,7 @@ type Scope interface { NoteScope(ctx context.Context) (predicate.Note, error) TenantScope(ctx context.Context) (predicate.Tenant, error) UserScope(ctx context.Context) (predicate.User, error) + SeatScope(ctx context.Context) (predicate.Seat, error) } // Unscoped is a [Scope] that narrows nothing. Embed it and write out the @@ -351,6 +353,9 @@ func (Unscoped) TenantScope(_ context.Context) (predicate.Tenant, error) { func (Unscoped) UserScope(_ context.Context) (predicate.User, error) { return nil, nil } +func (Unscoped) SeatScope(_ context.Context) (predicate.Seat, error) { + return nil, nil +} // Scopes is several [Scope]s at once: a row is in scope when every one of // them says so. @@ -491,6 +496,26 @@ func (ss Scopes) UserScope(ctx context.Context) (predicate.User, error) { return user.And(ps...), nil } +func (ss Scopes) SeatScope(ctx context.Context) (predicate.Seat, error) { + ps := make([]predicate.Seat, 0, len(ss)) + for _, s := range ss { + p, err := s.SeatScope(ctx) + if err != nil { + return nil, err + } + if p == nil { + continue + } + + ps = append(ps, p) + } + if len(ps) == 0 { + return nil, nil + } + + return seat.And(ps...), nil +} + // Minter decides the key a row is stored under. It is asked once per Add, // for an entity whose key is a uuid, and only for those. // @@ -621,3 +646,4 @@ func (s Server) MapField() apptest.MapFieldServiceServer { func (s Server) Note() apptest.NoteServiceServer { return NoteServiceServer{Store: s.Store} } func (s Server) Tenant() apptest.TenantServiceServer { return TenantServiceServer{Store: s.Store} } func (s Server) User() apptest.UserServiceServer { return UserServiceServer{Store: s.Store} } +func (s Server) Seat() apptest.SeatServiceServer { return SeatServiceServer{Store: s.Store} } diff --git a/internal/apptest/server/bare/x-indexedge_test.go b/internal/apptest/server/bare/x-indexedge_test.go new file mode 100644 index 0000000..93eccb9 --- /dev/null +++ b/internal/apptest/server/bare/x-indexedge_test.go @@ -0,0 +1,60 @@ +package bare_test + +import ( + "context" + "testing" + + "github.com/lesomnus/z" + pb "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// A unique index over edges named otherwise than what they point at finds its +// row by those edges. The lookup used to name each edge by its target's type +// -- `GetUser`, `HasUserWith` for an edge called `holder` -- and the bare +// server it generated did not compile. +func TestRefByAnIndexOverRenamedEdges(t *testing.T) { + t.Run("two edges", T(func(ctx context.Context, x *require.Assertions, c *Client) { + org, err := c.Tenant().Add(ctx, pb.TenantAddRequest_builder{}.Build()) + x.NoError(err) + // Users are unique by alias within a tenant. + holder, err := c.User().Add(ctx, pb.UserAddRequest_builder{Tenant: org.Ref(), Alias: z.Ptr("holder")}.Build()) + x.NoError(err) + other, err := c.User().Add(ctx, pb.UserAddRequest_builder{Tenant: org.Ref(), Alias: z.Ptr("other")}.Build()) + x.NoError(err) + + seat, err := c.Seat().Add(ctx, pb.SeatAddRequest_builder{Holder: holder.Ref(), Org: org.Ref()}.Build()) + x.NoError(err) + + got, err := c.Seat().Get(ctx, pb.SeatGetRequest_builder{ + Ref: pb.SeatRef_builder{Place: pb.SeatRefByPlace_builder{Holder: holder.Ref(), Org: org.Ref()}.Build()}.Build(), + }.Build()) + x.NoError(err) + x.Equal(seat.GetId(), got.GetId()) + + // Both edges are in the predicate, not only the first. + _, err = c.Seat().Get(ctx, pb.SeatGetRequest_builder{ + Ref: pb.SeatRef_builder{Place: pb.SeatRefByPlace_builder{Holder: other.Ref(), Org: org.Ref()}.Build()}.Build(), + }.Build()) + x.Equal(codes.NotFound, status.Code(err)) + })) + t.Run("one nullable edge", T(func(ctx context.Context, x *require.Assertions, c *Client) { + org, err := c.Tenant().Add(ctx, pb.TenantAddRequest_builder{}.Build()) + x.NoError(err) + holder, err := c.User().Add(ctx, pb.UserAddRequest_builder{Tenant: org.Ref(), Alias: z.Ptr("holder")}.Build()) + x.NoError(err) + deputy, err := c.User().Add(ctx, pb.UserAddRequest_builder{Tenant: org.Ref(), Alias: z.Ptr("deputy")}.Build()) + x.NoError(err) + + seat, err := c.Seat().Add(ctx, pb.SeatAddRequest_builder{Holder: holder.Ref(), Org: org.Ref(), Deputy: deputy.Ref()}.Build()) + x.NoError(err) + + got, err := c.Seat().Get(ctx, pb.SeatGetRequest_builder{ + Ref: pb.SeatRef_builder{Deputy: pb.SeatRefByDeputy_builder{Deputy: deputy.Ref()}.Build()}.Build(), + }.Build()) + x.NoError(err) + x.Equal(seat.GetId(), got.GetId()) + })) +} diff --git a/internal/apptest/store.g.go b/internal/apptest/store.g.go index 17e363c..2d83db6 100644 --- a/internal/apptest/store.g.go +++ b/internal/apptest/store.g.go @@ -32,6 +32,7 @@ type Server interface { Note() NoteServiceServer Tenant() TenantServiceServer User() UserServiceServer + Seat() SeatServiceServer } // RegisterServer registers every service of `s` with `g`. @@ -45,6 +46,7 @@ func RegisterServer(g grpc.ServiceRegistrar, s Server) { RegisterNoteServiceServer(g, s.Note()) RegisterTenantServiceServer(g, s.Tenant()) RegisterUserServiceServer(g, s.User()) + RegisterSeatServiceServer(g, s.Seat()) } type UnimplementedServer struct { @@ -54,6 +56,7 @@ type UnimplementedServer struct { NoteServer NoteServiceServer TenantServer TenantServiceServer UserServer UserServiceServer + SeatServer SeatServiceServer } func (UnimplementedServer) ValueField() ValueFieldServiceServer { @@ -68,6 +71,7 @@ func (UnimplementedServer) MapField() MapFieldServiceServer { func (UnimplementedServer) Note() NoteServiceServer { return UnimplementedNoteServiceServer{} } func (UnimplementedServer) Tenant() TenantServiceServer { return UnimplementedTenantServiceServer{} } func (UnimplementedServer) User() UserServiceServer { return UnimplementedUserServiceServer{} } +func (UnimplementedServer) Seat() SeatServiceServer { return UnimplementedSeatServiceServer{} } type StaticServer struct { ValueFieldServer ValueFieldServiceServer @@ -76,6 +80,7 @@ type StaticServer struct { NoteServer NoteServiceServer TenantServer TenantServiceServer UserServer UserServiceServer + SeatServer SeatServiceServer } func (s StaticServer) ValueField() ValueFieldServiceServer { return s.ValueFieldServer } @@ -84,6 +89,7 @@ func (s StaticServer) MapField() MapFieldServiceServer { return s.MapFie func (s StaticServer) Note() NoteServiceServer { return s.NoteServer } func (s StaticServer) Tenant() TenantServiceServer { return s.TenantServer } func (s StaticServer) User() UserServiceServer { return s.UserServer } +func (s StaticServer) Seat() SeatServiceServer { return s.SeatServer } type Client interface { ValueField() ValueFieldServiceClient @@ -92,6 +98,7 @@ type Client interface { Note() NoteServiceClient Tenant() TenantServiceClient User() UserServiceClient + Seat() SeatServiceClient } func NewClient(c *grpc.ClientConn) Client { @@ -102,6 +109,7 @@ func NewClient(c *grpc.ClientConn) Client { _Note: NewNoteServiceClient(c), _Tenant: NewTenantServiceClient(c), _User: NewUserServiceClient(c), + _Seat: NewSeatServiceClient(c), } } @@ -112,6 +120,7 @@ type client struct { _Note NoteServiceClient _Tenant TenantServiceClient _User UserServiceClient + _Seat SeatServiceClient } func (c *client) ValueField() ValueFieldServiceClient { return c._ValueField } @@ -120,6 +129,7 @@ func (c *client) MapField() MapFieldServiceClient { return c._MapField } func (c *client) Note() NoteServiceClient { return c._Note } func (c *client) Tenant() TenantServiceClient { return c._Tenant } func (c *client) User() UserServiceClient { return c._User } +func (c *client) Seat() SeatServiceClient { return c._Seat } // Middleware is a server that delegates to another server. type Middleware interface { diff --git a/proto/apptest/seat.proto b/proto/apptest/seat.proto new file mode 100644 index 0000000..789715a --- /dev/null +++ b/proto/apptest/seat.proto @@ -0,0 +1,59 @@ +edition = "2023"; + +package apptest; + +import "apptest/tenant.proto"; +import "apptest/user.proto"; +import "orm.proto"; + +option features.field_presence = IMPLICIT; +option go_package = "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest"; + +// Seat names its user and its tenant by edges whose names are not their +// targets' names, and is unique over them. A lookup by such an index reads +// each edge by its own name -- `GetHolder`, `HasHolderWith` -- because that is +// what the reference message and the ent predicates are named after; the +// target's name is only the name of the Pick that resolves it. +message Seat { + bytes id = 1 [(orm.field) = { + type: TYPE_UUID + key: true + default: "" + }]; + + User holder = 2 [(orm.edge) = {}]; + Tenant org = 3 [(orm.edge) = {}]; + // Nullable as well, which is the shape a login on a person has: at most + // one person per login, and a person with none. + User deputy = 4 [(orm.edge) = {nullable: true}]; + + option (orm.message) = { + rpc: {crud: true} + indexes: [ + { + name: "place" + refs: [ + { + name: "holder" + number: 2 + }, + { + name: "org" + number: 3 + } + ] + unique: true + }, + { + name: "deputy" + refs: [ + { + name: "deputy" + number: 4 + } + ] + unique: true + } + ] + }; +} diff --git a/proto/apptest/seat_svc.g.proto b/proto/apptest/seat_svc.g.proto new file mode 100644 index 0000000..d67e0c7 --- /dev/null +++ b/proto/apptest/seat_svc.g.proto @@ -0,0 +1,88 @@ +// Code generated by protoc-gen-orm-service. DO NOT EDIT. +// source: apptest/seat.proto + +edition = "2023"; + +package apptest; + +import "apptest/seat.proto"; +import "apptest/tenant_svc.g.proto"; +import "apptest/user_svc.g.proto"; +import "patch/patch.proto"; + +option go_package = "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest"; + +service SeatService { + // Add creates a new Seat + rpc Add(SeatAddRequest) returns (Seat); + // Get retrieves a Seat + rpc Get(SeatGetRequest) returns (Seat); + // Patch updates an existing Seat + rpc Patch(SeatPatchRequest) returns (Seat); + // Apply applies a patch document to an existing Seat + rpc Apply(SeatApplyRequest) returns (Seat); + // Erase deletes a Seat + rpc Erase(SeatRef) returns (SeatEraseResponse); +} + +message SeatAddRequest { + bytes id = 1; + UserRef holder = 2; + TenantRef org = 3; + UserRef deputy = 4; +} + +message SeatRefByPlace { + UserRef holder = 2; + TenantRef org = 3; +} + +message SeatRefByDeputy { + UserRef deputy = 4; +} + +message SeatRef { + oneof key { + bytes id = 1; + SeatRefByPlace place = 2; + SeatRefByDeputy deputy = 4; + } +} + +message SeatSelect { + bool all = 1; + UserSelect holder = 2; + TenantSelect org = 3; + UserSelect deputy = 4; +} + +message SeatGetRequest { + SeatRef ref = 1; + SeatSelect select = 2; +} + +message SeatPatchRequest { + SeatRef ref = 1; + UserRef holder = 4; + TenantRef org = 6; + UserRef deputy = 8; + // Clear deputy instead of writing it. + // It takes a field of its own because an unset value already means + // "leave it alone", so no value could have meant NULL. It wins + // outright: setting both this and deputy clears. + bool deputy_null = 9; +} + +message SeatApplyRequest { + SeatRef ref = 1; + patch.Patch patch = 2; +} + +message SeatEraseResponse { + // Erased is whether this call is the one that erased the row. + // + // False for a row that was already gone, was never there, or is out + // of this caller's reach -- which are one answer on purpose, and the + // reason the RPC does not fail instead. + bool erased = 1; +}