This is raw OpenFGA code:
await client.Write(
new ClientWriteRequest(
[
// Alice is an admin of form 123
new()
{
Object = "form:124",
Relation = "editor",
User = "user:avery",
},
]
)
);
var checkResponse = await client.Check(
new ClientCheckRequest
{
Object = "form:124",
Relation = "editor",
User = "user:avery",
}
);
var checkResponse2 = await client.Check(
new ClientCheckRequest
{
Object = "form:125",
Relation = "editor",
User = "user:avery",
}
);
This is an abstraction we wrote on top of it: await Permissions
.WithClient(client)
.ToMutate()
.Add<User, Form>("alice", "editor", "226")
.Add<User, Team>("alice", "member", "motion")
.SaveChangesAsync();
var allAllowed = await Permissions
.WithClient(client)
.ToValidate()
.Can<User, Form>("alice", "edit", "226")
.Has<User, Team>("alice", "member", "motion")
.ValidateAllAsync();
You would make the case that the former is better than the latter?
In the first example, I have to learn and understand OpenFGA, in the second example I have to learn and understand OpenFGA and your abstractions.
Well the point of using abstractions is that you don't need to know the things that it is abstracting. I think the abstraction here is self explaining what it does and you can certainly understand and use it without needing to understand all the specifics behind it.
More importantly: it prevents "usr:alice_123" instead of "user:alice_123" by using the type constraint to generate the prefix for the identifier.