Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ A `protoc`/`buf` plugin that generates **gRPC service definitions** (`.proto`)
from messages annotated with [protobuf-orm](https://github.com/protobuf-orm/protobuf-orm)
options.

You annotate an entity once; this plugin emits its `Add` / `Get` / `Patch` /
`Apply` / `Erase` service and the request/response messages those RPCs need. The output is
You annotate an entity once; this plugin emits its `Add` / `AddMany` / `Get` /
`Patch` / `Apply` / `Erase` service and the request/response messages those RPCs need. The output is
itself a `.proto` file, so it feeds back into your normal protoc pipeline (e.g.
`protoc-gen-go-grpc`, or [protoc-gen-orm-go](../protoc-gen-orm-go) /
[protoc-gen-orm-ent](../protoc-gen-orm-ent) for the implementation).
Expand Down Expand Up @@ -64,6 +64,29 @@ Highlights of the mapping:
[`patch.Patch`](https://github.com/lesomnus/protobuf-patch) document instead of
a field per prop, for the updates a fixed request shape cannot express — edit
one map key, splice a list, or test-then-set. See [Apply](#apply).
- **`UserAddManyRequest`** is there when the entity asks for AddMany. See
[AddMany](#addmany).

## AddMany

`add: {many: {}}` beside an entity's Add asks for AddMany, which adds many rows
in one call; `crud` alone does not:

```proto
option (orm.message) = {rpc: {crud: true, add: {many: {max: 200}}}};
```

```proto
rpc AddMany(UserAddManyRequest) returns (UserAddManyResponse);

message UserAddManyRequest { repeated UserAddRequest items = 1; }
message UserAddManyResponse { repeated User items = 1; }
```

An item is the entity's own `UserAddRequest`, so whatever reads an Add's request
reads an item the same way. The rows come back in the order of the items, and
the call is all of them or none. `max` is the most items one call may carry,
1000 when unset; the comment on the RPC says which.

## Apply

Expand Down
3 changes: 3 additions & 0 deletions app/work.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ func (w *work) run(ctx context.Context, f *ast.File, entity graph.Entity) error
if rpcs.HasAdd() {
fw.xRpcAdd()
}
if rpcs.HasAddMany() {
fw.xRpcAddMany()
}
if rpcs.HasGet() {
fw.xRpcGet()
}
Expand Down
40 changes: 40 additions & 0 deletions app/x-add.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app

import (
"fmt"

"github.com/protobuf-orm/protobuf-orm/graph"
"github.com/protobuf-orm/protoc-gen-orm-service/internal/ast"
)
Expand All @@ -16,6 +18,44 @@ func (w *fileWork) xRpcAdd() ast.Rpc {
)
}

func (w *fileWork) xRpcAddMany() ast.Rpc {
v := w.entity.Rpcs().GetAddMany()
return w.defineRpc(
ast.Comment(fmt.Sprintf("AddMany creates up to %d new %s rows in one call, all of them or none", v.Max(), w.entity.Name())),
ast.Rpc{
Name: "AddMany",
RequestType: w.xMsgAddManyRequest().Name,
ResponseType: w.xMsgAddManyResponse().Name,
},
)
}

// xMsgAddManyRequest is a list of the entity's own AddRequest rather than a
// message of its own, so that an item is an Add's request field for field and
// whatever reads one reads the other.
func (w *fileWork) xMsgAddManyRequest() ast.Message {
return w.defineMsg("AddManyRequest", func(m *ast.Message) {
m.Body = append(m.Body, ast.MessageField{
Name: "items",
Number: 1,
Label: ast.LabelRepeated,
Type: w.xMsgAddRequest().Name,
})
})
}

// xMsgAddManyResponse is the rows the items added, in the order of the items.
func (w *fileWork) xMsgAddManyResponse() ast.Message {
return w.defineMsg("AddManyResponse", func(m *ast.Message) {
m.Body = append(m.Body, ast.MessageField{
Name: "items",
Number: 1,
Label: ast.LabelRepeated,
Type: w.useEntityType(w.entity),
})
})
}

func (w *fileWork) xMsgAddRequest() ast.Message {
return w.defineMsg("AddRequest", func(m *ast.Message) {
for p := range w.entity.Props() {
Expand Down
4 changes: 2 additions & 2 deletions buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
version: v2
deps:
- name: buf.build/orm/orm
commit: 57ec25ec8eca4aed9075f5eb3313576d
digest: b5:9f6d5ab844a3836eced550085ee6b15bf4056326c945774a9b3ecb1ce179191478dea4dc425ad1e71fc9b919424530c92c8977eab19f10ce8e727dd4495ae12e
commit: 6bd3c068f80b4b64b8f82104a8d2fb7d
digest: b5:4cfb6f0910912edc31990604881e9097aa0cbbc9c16e2389aa192bcaa4de8175e8737dfdb36abd8d8d40a542dc176a9f6972de824f59d148f247e5a3d4b26ea1
- name: buf.build/patch/patch
commit: 1ff59ac3af634b2b9b73cc7ac1f9a3bd
digest: b5:db6b10aecb55283c840f3018b0aa6d601dc160c29db836109cbca622e4bcd4771b04b805f858c949dfac9d0c0c6056da83934678430c8f7d6059db33202b8650
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.27.0

require (
github.com/ettle/strcase v0.2.0
github.com/protobuf-orm/protobuf-orm v0.0.0-20260906212449-04c0cd58f10a
github.com/protobuf-orm/protobuf-orm v0.0.0-20261017212716-3430237a8656
github.com/stretchr/testify v1.10.0
google.golang.org/protobuf v1.36.11
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ github.com/protobuf-orm/protobuf-orm v0.0.0-20260901155106-6bf45a2a1e67 h1:ERfia
github.com/protobuf-orm/protobuf-orm v0.0.0-20260901155106-6bf45a2a1e67/go.mod h1:OYlbo4/9t+Hgj/XZwdgN8Z9MyViTLPHxBWLUR4+j52o=
github.com/protobuf-orm/protobuf-orm v0.0.0-20260906212449-04c0cd58f10a h1:EXbJ28yY25wzt2HbzVkw7Tk46pF1GAHV+XMcC+kir40=
github.com/protobuf-orm/protobuf-orm v0.0.0-20260906212449-04c0cd58f10a/go.mod h1:OYlbo4/9t+Hgj/XZwdgN8Z9MyViTLPHxBWLUR4+j52o=
github.com/protobuf-orm/protobuf-orm v0.0.0-20261017194719-b1318979edf2 h1:5egMGCQDXK83woO1ZC3s+tydei9ViXFoDbRBcsb4TKY=
github.com/protobuf-orm/protobuf-orm v0.0.0-20261017194719-b1318979edf2/go.mod h1:OYlbo4/9t+Hgj/XZwdgN8Z9MyViTLPHxBWLUR4+j52o=
github.com/protobuf-orm/protobuf-orm v0.0.0-20261017212716-3430237a8656 h1:dVr9HD7HgeUh1c+xhTfjKzwfpKTUDNMcVfMPOt8HPkI=
github.com/protobuf-orm/protobuf-orm v0.0.0-20261017212716-3430237a8656/go.mod h1:OYlbo4/9t+Hgj/XZwdgN8Z9MyViTLPHxBWLUR4+j52o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
Expand Down
40 changes: 40 additions & 0 deletions internal/apptest/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@ func TestRpcApplyIsGatedOnPatch(t *testing.T) {
}
}

// AddMany is a list of the entity's own AddRequest in and the rows out, so that
// an item is an Add's request field for field, and the comment says the most
// one call carries.
func TestRpcAddMany(t *testing.T) {
x := require.New(t)

files := generate(t)
src, ok := files["apptest/user_svc.g.proto"]
x.True(ok, "generated: %v", slices.Sorted(maps.Keys(files)))

x.Contains(src, strings.Join([]string{
"\t// AddMany creates up to 200 new User rows in one call, all of them or none",
"\trpc AddMany(UserAddManyRequest) returns (UserAddManyResponse);",
}, "\n"))
x.Contains(src, strings.Join([]string{
"message UserAddManyRequest {",
"\trepeated UserAddRequest items = 1;",
"}",
}, "\n"))
x.Contains(src, strings.Join([]string{
"message UserAddManyResponse {",
"\trepeated User items = 1;",
"}",
}, "\n"))
}

// And only where the entity asked for it: `crud` alone is no AddMany.
func TestRpcAddManyIsAskedFor(t *testing.T) {
x := require.New(t)

files := generate(t)
for path, src := range files {
if path == "apptest/user_svc.g.proto" {
continue
}
x.NotContains(src, "AddMany", "%s has an AddMany it did not ask for", path)
}
x.Contains(files["apptest/tenant_svc.g.proto"], "rpc Add(")
}

// An entity with an edge to its own type selects the far end the same way it
// selects itself, and two entities of one file with edges to each other each
// select the other. Generating either used to recurse until the stack ran out.
Expand Down
7 changes: 6 additions & 1 deletion proto/apptest/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ message User {
}];

option (orm.message) = {
rpc: {crud: true}
rpc: {
crud: true
add: {
many: {max: 200}
}
}
indexes: [
{
name: "alias"
Expand Down
10 changes: 10 additions & 0 deletions proto/apptest/user_svc.g.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ option go_package = "github.com/protobuf-orm/protoc-gen-orm-service/internal/app
service UserService {
// Add creates a new User
rpc Add(UserAddRequest) returns (User);
// AddMany creates up to 200 new User rows in one call, all of them or none
rpc AddMany(UserAddManyRequest) returns (UserAddManyResponse);
// Get retrieves a User
rpc Get(UserGetRequest) returns (User);
// Patch updates an existing User
Expand All @@ -36,6 +38,14 @@ message UserAddRequest {
google.protobuf.Timestamp date_created = 15;
}

message UserAddManyRequest {
repeated UserAddRequest items = 1;
}

message UserAddManyResponse {
repeated User items = 1;
}

message UserRefByAlias {
string alias = 4;
Tenant tenant = 2;
Expand Down
Loading