1use crate::model::{
7 entity::EntityModel,
8 field::{FieldKind, RelationStrength},
9};
10use candid::CandidType;
11use serde::{Deserialize, Serialize};
12
13#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
21pub struct EntitySchemaDescription {
22 pub entity_path: String,
23 pub entity_name: String,
24 pub primary_key: String,
25 pub fields: Vec<EntityFieldDescription>,
26 pub indexes: Vec<EntityIndexDescription>,
27 pub relations: Vec<EntityRelationDescription>,
28}
29
30#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38pub struct EntityFieldDescription {
39 pub name: String,
40 pub kind: String,
41 pub primary_key: bool,
42 pub queryable: bool,
43}
44
45#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
53pub struct EntityIndexDescription {
54 pub name: String,
55 pub unique: bool,
56 pub fields: Vec<String>,
57}
58
59#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct EntityRelationDescription {
68 pub field: String,
69 pub target_path: String,
70 pub target_entity_name: String,
71 pub target_store_path: String,
72 pub strength: EntityRelationStrength,
73 pub cardinality: EntityRelationCardinality,
74}
75
76#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub enum EntityRelationStrength {
83 Strong,
84 Weak,
85}
86
87#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub enum EntityRelationCardinality {
94 Single,
95 List,
96 Set,
97}
98
99#[must_use]
101pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
102 let mut fields = Vec::with_capacity(model.fields.len());
103 let mut relations = Vec::new();
104 for field in model.fields {
105 let field_kind = summarize_field_kind(&field.kind);
106 let queryable = field.kind.value_kind().is_queryable();
107 let primary_key = field.name == model.primary_key.name;
108
109 fields.push(EntityFieldDescription {
110 name: field.name.to_string(),
111 kind: field_kind,
112 primary_key,
113 queryable,
114 });
115
116 if let Some(relation) = relation_from_field_kind(field.name, &field.kind) {
117 relations.push(relation);
118 }
119 }
120
121 let mut indexes = Vec::with_capacity(model.indexes.len());
122 for index in model.indexes {
123 indexes.push(EntityIndexDescription {
124 name: index.name.to_string(),
125 unique: index.unique,
126 fields: index
127 .fields
128 .iter()
129 .map(|field| (*field).to_string())
130 .collect(),
131 });
132 }
133
134 EntitySchemaDescription {
135 entity_path: model.path.to_string(),
136 entity_name: model.entity_name.to_string(),
137 primary_key: model.primary_key.name.to_string(),
138 fields,
139 indexes,
140 relations,
141 }
142}
143
144fn relation_from_field_kind(
146 field_name: &str,
147 kind: &FieldKind,
148) -> Option<EntityRelationDescription> {
149 match kind {
150 FieldKind::Relation {
151 target_path,
152 target_entity_name,
153 target_store_path,
154 strength,
155 ..
156 } => Some(EntityRelationDescription {
157 field: field_name.to_string(),
158 target_path: (*target_path).to_string(),
159 target_entity_name: (*target_entity_name).to_string(),
160 target_store_path: (*target_store_path).to_string(),
161 strength: relation_strength(*strength),
162 cardinality: EntityRelationCardinality::Single,
163 }),
164 FieldKind::List(inner) => {
165 relation_from_collection_relation(field_name, inner, EntityRelationCardinality::List)
166 }
167 FieldKind::Set(inner) => {
168 relation_from_collection_relation(field_name, inner, EntityRelationCardinality::Set)
169 }
170 FieldKind::Account
171 | FieldKind::Blob
172 | FieldKind::Bool
173 | FieldKind::Date
174 | FieldKind::Decimal { .. }
175 | FieldKind::Duration
176 | FieldKind::Enum { .. }
177 | FieldKind::Float32
178 | FieldKind::Float64
179 | FieldKind::Int
180 | FieldKind::Int128
181 | FieldKind::IntBig
182 | FieldKind::Principal
183 | FieldKind::Subaccount
184 | FieldKind::Text
185 | FieldKind::Timestamp
186 | FieldKind::Uint
187 | FieldKind::Uint128
188 | FieldKind::UintBig
189 | FieldKind::Ulid
190 | FieldKind::Unit
191 | FieldKind::Map { .. }
192 | FieldKind::Structured { .. } => None,
193 }
194}
195
196fn relation_from_collection_relation(
198 field_name: &str,
199 inner: &FieldKind,
200 cardinality: EntityRelationCardinality,
201) -> Option<EntityRelationDescription> {
202 let FieldKind::Relation {
203 target_path,
204 target_entity_name,
205 target_store_path,
206 strength,
207 ..
208 } = inner
209 else {
210 return None;
211 };
212
213 Some(EntityRelationDescription {
214 field: field_name.to_string(),
215 target_path: (*target_path).to_string(),
216 target_entity_name: (*target_entity_name).to_string(),
217 target_store_path: (*target_store_path).to_string(),
218 strength: relation_strength(*strength),
219 cardinality,
220 })
221}
222
223const fn relation_strength(strength: RelationStrength) -> EntityRelationStrength {
225 match strength {
226 RelationStrength::Strong => EntityRelationStrength::Strong,
227 RelationStrength::Weak => EntityRelationStrength::Weak,
228 }
229}
230
231fn summarize_field_kind(kind: &FieldKind) -> String {
233 match kind {
234 FieldKind::Account => "account".to_string(),
235 FieldKind::Blob => "blob".to_string(),
236 FieldKind::Bool => "bool".to_string(),
237 FieldKind::Date => "date".to_string(),
238 FieldKind::Decimal { scale } => format!("decimal(scale={scale})"),
239 FieldKind::Duration => "duration".to_string(),
240 FieldKind::Enum { path } => format!("enum({path})"),
241 FieldKind::Float32 => "float32".to_string(),
242 FieldKind::Float64 => "float64".to_string(),
243 FieldKind::Int => "int".to_string(),
244 FieldKind::Int128 => "int128".to_string(),
245 FieldKind::IntBig => "int_big".to_string(),
246 FieldKind::Principal => "principal".to_string(),
247 FieldKind::Subaccount => "subaccount".to_string(),
248 FieldKind::Text => "text".to_string(),
249 FieldKind::Timestamp => "timestamp".to_string(),
250 FieldKind::Uint => "uint".to_string(),
251 FieldKind::Uint128 => "uint128".to_string(),
252 FieldKind::UintBig => "uint_big".to_string(),
253 FieldKind::Ulid => "ulid".to_string(),
254 FieldKind::Unit => "unit".to_string(),
255 FieldKind::Relation {
256 target_entity_name,
257 key_kind,
258 strength,
259 ..
260 } => format!(
261 "relation(target={target_entity_name}, key={}, strength={})",
262 summarize_field_kind(key_kind),
263 summarize_relation_strength(*strength),
264 ),
265 FieldKind::List(inner) => format!("list<{}>", summarize_field_kind(inner)),
266 FieldKind::Set(inner) => format!("set<{}>", summarize_field_kind(inner)),
267 FieldKind::Map { key, value } => {
268 format!(
269 "map<{}, {}>",
270 summarize_field_kind(key),
271 summarize_field_kind(value)
272 )
273 }
274 FieldKind::Structured { queryable } => format!("structured(queryable={queryable})"),
275 }
276}
277
278const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
280 match strength {
281 RelationStrength::Strong => "strong",
282 RelationStrength::Weak => "weak",
283 }
284}