1mod wire;
4
5use std::sync::Arc;
6
7use wire::{RacialSpellRow, RotationScriptRow, SpecializationSpellIdRow, SpellOverrideRow};
8use wowlab_engine_domain::dbc::{ItemClass, item_matches_consumable_query};
9use wowlab_engine_ports::{
10 DataResolver, EnchantmentRow, ResolverError, SpellId, SpellSearchResult,
11};
12use wowlab_supabase::SupabaseClient;
13use wowlab_types::data::{
14 ChallengeModeHealthFlat, ContentTuningFlat, ContentTuningXDifficultyFlat,
15 ContentTuningXExpectedFlat, CreatureDifficultyFlat, CreatureFlat, ExpansionTraitTreeFlat,
16 ExpectedStatFlat, ExpectedStatModFlat, ItemDamageScalingFlat, ItemDataFlat, ItemScalingData,
17 PowerTypeFlat, SpecDataFlat, SpellDataFlat, SpellEffect, TraitTreeFlat,
18};
19
20use super::{
21 cache::GameDataCache,
22 query::{Query, Table},
23};
24use crate::rows::EnchantmentRowWire;
25
26const CONSUMABLE_NAME_PATTERN_CAPACITY_MULTIPLIER: usize = 2;
27
28fn backend_error<E>(op: &'static str) -> impl FnOnce(E) -> ResolverError
29where
30 E: std::error::Error + Send + Sync + 'static,
31{
32 move |source| ResolverError::backend(op, source)
33}
34
35fn consumable_name_pattern(name_token: &str) -> String {
36 let capacity = name_token
37 .len()
38 .saturating_mul(CONSUMABLE_NAME_PATTERN_CAPACITY_MULTIPLIER);
39 let mut pattern = String::with_capacity(capacity);
40
41 for character in name_token.chars().filter(char::is_ascii_alphanumeric) {
42 if !pattern.is_empty() {
43 pattern.push('*');
44 }
45
46 pattern.push(character);
47 }
48
49 pattern
50}
51
52fn consumable_items_path(name_token: &str, subclass: i32) -> String {
53 Query::from(Table::Items)
54 .eq("class_id", ItemClass::Consumable as i32)
55 .eq("subclass_id", subclass)
56 .ilike_contains("name", &consumable_name_pattern(name_token))
57 .into_path()
58}
59
60fn matching_consumable_items(
61 items: Vec<ItemDataFlat>,
62 name_token: &str,
63 subclass: i32,
64) -> Vec<ItemDataFlat> {
65 items
66 .into_iter()
67 .filter(|item| item_matches_consumable_query(item, name_token, subclass))
68 .collect()
69}
70
71#[derive(Debug)]
73pub struct SupabaseResolver {
74 cache: GameDataCache,
75}
76
77impl SupabaseResolver {
78 #[must_use]
80 pub fn new(cache: GameDataCache) -> Self {
81 Self { cache }
82 }
83
84 pub(crate) fn client(&self) -> &SupabaseClient {
85 self.cache.client()
86 }
87
88 async fn fetch_rotation(&self, path: &str, label: &str) -> Result<String, ResolverError> {
89 let rows: Vec<RotationScriptRow> = self
90 .client()
91 .get_json(path, "game")
92 .await
93 .map_err(backend_error("fetch rotation"))?;
94
95 rows.into_iter()
96 .next()
97 .map(|r| r.script.to_string())
98 .ok_or_else(|| ResolverError::rotation_script_not_found(label))
99 }
100}
101
102impl DataResolver for SupabaseResolver {
103 async fn get_spell(&self, spell_id: SpellId) -> Result<SpellDataFlat, ResolverError> {
104 self.cache
105 .get_spell(spell_id.as_i32())
106 .await
107 .map_err(backend_error("fetch spell"))
108 }
109
110 async fn get_spells(&self, spell_ids: &[SpellId]) -> Result<Vec<SpellDataFlat>, ResolverError> {
111 let raw_ids: Vec<i32> = spell_ids.iter().map(SpellId::as_i32).collect();
112
113 self.cache
114 .get_spells(&raw_ids)
115 .await
116 .map_err(backend_error("fetch spells"))
117 }
118
119 async fn get_spell_effect(
120 &self,
121 spell_id: SpellId,
122 effect_index: u8,
123 ) -> Result<SpellEffect, ResolverError> {
124 let spell = self.get_spell(spell_id).await?;
125
126 crate::rows::validate_effect_index(spell_id, effect_index, &spell.effects)
127 }
128
129 async fn get_spell_effects(
130 &self,
131 spell_id: SpellId,
132 ) -> Result<Vec<SpellEffect>, ResolverError> {
133 let spell = self.get_spell(spell_id).await?;
134
135 Ok(spell.effects.clone())
136 }
137
138 async fn get_item(&self, item_id: i32) -> Result<ItemDataFlat, ResolverError> {
139 self.cache
140 .get_item(item_id)
141 .await
142 .map_err(backend_error("fetch item"))
143 }
144
145 async fn find_consumable_items(
146 &self,
147 name_token: &str,
148 subclass: i32,
149 ) -> Result<Vec<ItemDataFlat>, ResolverError> {
150 let path = consumable_items_path(name_token, subclass);
151 let items: Vec<ItemDataFlat> = self
152 .client()
153 .get_json(&path, "game")
154 .await
155 .map_err(backend_error("find consumable items"))?;
156
157 Ok(matching_consumable_items(items, name_token, subclass))
158 }
159
160 async fn get_scaling_data(&self) -> Result<Arc<ItemScalingData>, ResolverError> {
161 Box::pin(self.cache.get_scaling_data())
162 .await
163 .map_err(backend_error("fetch scaling data"))
164 }
165
166 async fn get_power_types(&self) -> Result<Vec<PowerTypeFlat>, ResolverError> {
167 self.cache
168 .get_power_types()
169 .await
170 .map_err(backend_error("fetch power types"))
171 }
172
173 async fn get_spec(&self, spec_id: i32) -> Result<SpecDataFlat, ResolverError> {
174 self.cache
175 .get_spec(spec_id)
176 .await
177 .map_err(backend_error("fetch specialization"))
178 }
179
180 async fn get_trait_tree(&self, spec_id: i32) -> Result<TraitTreeFlat, ResolverError> {
181 self.cache
182 .get_trait_tree(spec_id)
183 .await
184 .map_err(backend_error("fetch trait tree"))
185 }
186
187 async fn get_expansion_trait_tree(
188 &self,
189 expansion_id: i32,
190 system: &str,
191 ) -> Result<ExpansionTraitTreeFlat, ResolverError> {
192 let path = Query::from(Table::ExpansionTraits)
193 .eq("expansion_id", expansion_id)
194 .eq("system", system)
195 .limit(1)
196 .into_path();
197 let mut rows: Vec<ExpansionTraitTreeFlat> = self
198 .client()
199 .get_json(&path, "game")
200 .await
201 .map_err(backend_error("fetch expansion trait tree"))?;
202
203 rows.pop()
204 .ok_or_else(|| ResolverError::expansion_trait_tree_not_found(expansion_id, system))
205 }
206
207 async fn get_rotation_script(&self, rotation_id: &str) -> Result<String, ResolverError> {
208 let path = Query::from(Table::Rotations)
209 .eq("id", rotation_id)
210 .select("script")
211 .into_path();
212
213 tracing::debug!(rotation_id, "Fetching rotation script");
214
215 self.fetch_rotation(&path, rotation_id).await
216 }
217
218 async fn get_spell_overrides(
219 &self,
220 spec_id: i32,
221 ) -> Result<Vec<(SpellId, SpellId)>, ResolverError> {
222 let path = Query::from(Table::SpecializationSpells)
223 .eq("spec_id", spec_id)
224 .gt("overrides_spell_id", 0)
225 .select("overrides_spell_id,spell_id")
226 .into_path();
227
228 let rows: Vec<SpellOverrideRow> = self
229 .client()
230 .get_json(&path, "game")
231 .await
232 .map_err(backend_error("fetch spell overrides"))?;
233
234 Ok(crate::rows::spell_override_pairs(
235 rows.into_iter().map(|r| (r.overrides_spell_id, r.spell_id)),
236 ))
237 }
238
239 async fn get_specialization_spells(&self, spec_id: i32) -> Result<Vec<SpellId>, ResolverError> {
240 let path = Query::from(Table::SpecializationSpells)
241 .eq("spec_id", spec_id)
242 .select("spell_id")
243 .into_path();
244
245 let rows: Vec<SpecializationSpellIdRow> = self
246 .client()
247 .get_json(&path, "game")
248 .await
249 .map_err(backend_error("fetch specialization spells"))?;
250
251 Ok(rows
252 .into_iter()
253 .map(|row| SpellId::new(row.spell_id))
254 .collect())
255 }
256
257 async fn get_racial_spells(
258 &self,
259 race_id: i32,
260 class_id: i32,
261 ) -> Result<Vec<SpellId>, ResolverError> {
262 let path = Query::from(Table::RacialSpells)
263 .eq("race_id", race_id)
264 .select("spell_id,class_mask")
265 .into_path();
266
267 let rows: Vec<RacialSpellRow> = self
268 .client()
269 .get_json(&path, "game")
270 .await
271 .map_err(backend_error("fetch racial spells"))?;
272
273 Ok(crate::rows::racial_spell_ids(
274 rows.into_iter().map(|row| (row.spell_id, row.class_mask)),
275 class_id,
276 ))
277 }
278
279 async fn get_expected_stats(
280 &self,
281 expansion_id: i32,
282 lvl: i32,
283 ) -> Result<ExpectedStatFlat, ResolverError> {
284 let path = Query::from(Table::ExpectedStats)
285 .eq("expansion_id", expansion_id)
286 .eq("lvl", lvl)
287 .limit(1)
288 .into_path();
289 let rows: Vec<ExpectedStatFlat> = self
290 .client()
291 .get_json(&path, "game")
292 .await
293 .map_err(backend_error("fetch expected stats"))?;
294
295 rows.into_iter()
296 .next()
297 .ok_or_else(|| ResolverError::expected_stats_not_found(expansion_id, lvl))
298 }
299
300 async fn get_item_damage_scaling(
301 &self,
302 item_level: i32,
303 weapon_type: &str,
304 ) -> Result<ItemDamageScalingFlat, ResolverError> {
305 let path = Query::from(Table::ItemDamageScaling)
306 .eq("item_level", item_level)
307 .eq("weapon_type", weapon_type)
308 .limit(1)
309 .into_path();
310 let rows: Vec<ItemDamageScalingFlat> = self
311 .client()
312 .get_json(&path, "game")
313 .await
314 .map_err(backend_error("fetch item damage scaling"))?;
315
316 rows.into_iter()
317 .next()
318 .ok_or_else(|| ResolverError::item_damage_scaling_not_found(weapon_type, item_level))
319 }
320
321 async fn get_enchantment(&self, enchantment_id: i32) -> Result<EnchantmentRow, ResolverError> {
322 let path = Query::from(Table::Enchantments)
323 .eq("id", enchantment_id)
324 .limit(1)
325 .into_path();
326 let rows: Vec<EnchantmentRowWire> = self
327 .client()
328 .get_json(&path, "game")
329 .await
330 .map_err(backend_error("fetch enchantment"))?;
331
332 rows.into_iter()
333 .next()
334 .map(crate::rows::enchantment_row)
335 .ok_or_else(|| ResolverError::enchantment_not_found(enchantment_id))
336 }
337
338 async fn search_spells(
339 &self,
340 query: &str,
341 limit: u32,
342 ) -> Result<Vec<SpellSearchResult>, ResolverError> {
343 let path = Query::from(Table::Spells)
344 .ilike_contains("name", query)
345 .select("id,name")
346 .limit(limit)
347 .into_path();
348 let results: Vec<SpellSearchResult> = self
349 .client()
350 .get_json(&path, "game")
351 .await
352 .map_err(backend_error("search spells"))?;
353
354 Ok(results)
355 }
356
357 async fn get_creature(&self, creature_id: i32) -> Result<CreatureFlat, ResolverError> {
358 let path = Query::from(Table::Creatures)
359 .eq("id", creature_id)
360 .limit(1)
361 .into_path();
362 let rows: Vec<CreatureFlat> = self
363 .client()
364 .get_json(&path, "game")
365 .await
366 .map_err(backend_error("fetch creature"))?;
367
368 rows.into_iter()
369 .next()
370 .ok_or_else(|| ResolverError::creature_not_found(creature_id))
371 }
372
373 async fn get_creature_difficulties(
374 &self,
375 creature_id: i32,
376 ) -> Result<Vec<CreatureDifficultyFlat>, ResolverError> {
377 let path = Query::from(Table::CreatureDifficulties)
378 .eq("creature_id", creature_id)
379 .into_path();
380
381 self.client()
382 .get_json(&path, "game")
383 .await
384 .map_err(backend_error("fetch creature difficulties"))
385 }
386
387 async fn get_content_tuning(
388 &self,
389 content_tuning_id: i32,
390 ) -> Result<ContentTuningFlat, ResolverError> {
391 let path = Query::from(Table::ContentTunings)
392 .eq("id", content_tuning_id)
393 .limit(1)
394 .into_path();
395 let rows: Vec<ContentTuningFlat> = self
396 .client()
397 .get_json(&path, "game")
398 .await
399 .map_err(backend_error("fetch content tuning"))?;
400
401 rows.into_iter()
402 .next()
403 .ok_or_else(|| ResolverError::content_tuning_not_found(content_tuning_id))
404 }
405
406 async fn get_content_tuning_x_difficulty(
407 &self,
408 content_tuning_id: i32,
409 ) -> Result<Vec<ContentTuningXDifficultyFlat>, ResolverError> {
410 let path = Query::from(Table::ContentTuningXDifficulty)
411 .eq("content_tuning_id", content_tuning_id)
412 .into_path();
413
414 self.client()
415 .get_json(&path, "game")
416 .await
417 .map_err(backend_error("fetch content tuning difficulties"))
418 }
419
420 async fn get_content_tuning_x_expected(
421 &self,
422 content_tuning_id: i32,
423 ) -> Result<Vec<ContentTuningXExpectedFlat>, ResolverError> {
424 let path = Query::from(Table::ContentTuningXExpected)
425 .eq("content_tuning_id", content_tuning_id)
426 .into_path();
427
428 self.client()
429 .get_json(&path, "game")
430 .await
431 .map_err(backend_error("fetch content tuning expected stats"))
432 }
433
434 async fn get_expected_stat_mod(
435 &self,
436 expected_stat_mod_id: i32,
437 ) -> Result<ExpectedStatModFlat, ResolverError> {
438 let path = Query::from(Table::ExpectedStatMods)
439 .eq("id", expected_stat_mod_id)
440 .limit(1)
441 .into_path();
442 let rows: Vec<ExpectedStatModFlat> = self
443 .client()
444 .get_json(&path, "game")
445 .await
446 .map_err(backend_error("fetch expected stat modifier"))?;
447
448 rows.into_iter()
449 .next()
450 .ok_or_else(|| ResolverError::expected_stat_mod_not_found(expected_stat_mod_id))
451 }
452
453 async fn get_challenge_mode_health(
454 &self,
455 keystone_level: i32,
456 ) -> Result<ChallengeModeHealthFlat, ResolverError> {
457 let path = Query::from(Table::ChallengeModeHealth)
458 .eq("challenge_level", keystone_level)
459 .limit(1)
460 .into_path();
461 let rows: Vec<ChallengeModeHealthFlat> = self
462 .client()
463 .get_json(&path, "game")
464 .await
465 .map_err(backend_error("fetch challenge mode health"))?;
466
467 rows.into_iter()
468 .next()
469 .ok_or_else(|| ResolverError::challenge_mode_health_not_found(keystone_level))
470 }
471}
472
473#[cfg(test)]
474mod tests;