The generate model command
The generate model
command generates a Protobuf message that will act as a "view model", being the representation of
a record transferred over the wire to your gRPC network service, and a bucket definition for storage.
Model records must be identified by a string-based surrogate key, such as an XID or UUID, to be compatible with modern scale-out storage systems.
The generate model
command is used like this:
$ saas-rs generate model invoice --service user --version 1 customer_id address_1 address_2 city state zip postal_code country_iso2
$ make
$ git add -A
$ git commit -m "saas-rs generate model invoice --service user --version 1 customer_id address_1 address_2 city state zip postal_code country_iso2"
The following content is added or changed in your Rust workspace:
crates/
├── config_store/
│ ├── src/
│ │ └── bucket.rs
├── protocol/
│ ├── src/
│ │ └── generated/
│ │ ├── acme_user_v1.rs
│ │ └── acme_user_v1_serde.rs
proto/
└── acme/
└── user/
└── v1/
├── invoice.proto
└── user_service.proto
An examination of the proto/acme/user/v1/invoice.proto
file shows the generated view model:
syntax = "proto3";
package acme.user.v1;
import "google/protobuf/timestamp.proto";
message Invoice {
string id = 1;
string customer_id = 2;
string address_1 = 3;
string address_2 = 4;
string city = 5;
string state = 6;
string zip = 7;
string postal_code = 8;
string country_iso2 = 9;
google.protobuf.Timestamp created_at = 1000;
optional string created_by_account_id = 1001;
optional google.protobuf.Timestamp deleted_at = 1002;
optional string deleted_by_account_id = 1003;
optional google.protobuf.Timestamp updated_at = 1004;
optional string updated_by_account_id = 1005;
oneof owner {
string owner_account_id = 1006;
}
}
And for this new Protobuf file to be found by the Prost code generator, it needs to be referenced by the
proto/acme/user/v1/user_sevice.proto
file:
import "acme/user/v1/invoice.proto";
Also notice that a Bucket enum variant was added in crates/config_store/src/bucket.rs
, which helps the storage layer
understand that this bucket represents:
- a new table, if you'll be using an RDBMS based ConfigStore
- a new collection, if you'll be using a Document based ConfigStore
- or new a key prefix, if you'll be using a KV based ConfigStore
pub enum Bucket {
#[strum(serialize = "invoices")]
Invoices,
...
}
Storage models
View models are translated into storage models before being used with a storage adapter. Storage models are general-purpose BSON documents instead of concrete structs, so that you don't have to redundantly define both view models and storge models. View models are converted to/from storage models with help from the pbbson crate. Storage models will be elaborated in another section.
Compound model names
It's perfectly valid to define a new model with a compound name, such as LinkedAccount
or InvoiceChangeAction
.
When the generate model
command is used like this:
$ saas-rs generate model invoice-change-action --service user --version 1
The model filename is automatically snake-cased to:
proto/acme/user/v1/invoice_change_action.proto
And the bucket enum variant's strum serialize attribute defines a camel-cased + pluralized bucket name for KV and Document stores, while the enum variant identifier itself is pascal-cased + pluralized:
pub enum Bucket {
#[strum(serialize = "invoiceChangeActions")]
InvoiceChangeActions,
...
Making Further Changes
The fields initially generated are completely customizable. Only the id
and audit fields are expected to remain
unaltered. So there's no difference between invoking the generate model
command with a complete field list, or
invoking it with an empty list and typing in fields by hand.
Some common changes to models include:
- setting fields as
optional
- tweaking the datatypes of the generated fields
- adding additional audit fields, such as
last_viewed_by
andlast_viewed_at
, orapproved_by
andapproved_at