Compare commits

..
Author SHA1 Message Date
Brandon Thomas 8e3f6628c1 docs 2026-06-24 00:23:52 -04:00
Brandon Thomas 214eda6818 update docs 2026-06-24 00:21:07 -04:00
Brandon Thomas 47648b19ae remove fields, document 2026-06-23 23:46:18 -04:00
Brandon Thomas 379e528e0a new endpoints 2026-06-23 23:36:35 -04:00
Brandon Thomas 9965e2544d cleanup docs 2026-06-23 23:19:55 -04:00
937 changed files with 17293 additions and 37848 deletions
@@ -1,25 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT token AS `token: UserToken`\nFROM users\nWHERE email_address = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "0d2eadc64240b9ce5348b3b238b0ae6229df21697c952b08e930d03f97414cf4"
}
@@ -1,58 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n amount_usd_cents,\n event_type AS `event_type!: PaymentEventType`,\n maybe_credits_granted AS `maybe_credits_granted: u32`\nFROM user_spend_events\nWHERE maybe_user_token = ?\n AND payments_namespace = ?\n AND is_production = TRUE\nORDER BY payment_occurred_at ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 1,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 2,
"name": "event_type!: PaymentEventType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "1c997019b7ed18059bc08d857918c3ce3f5e6ed56d358894cbc496d345566519"
}
@@ -1,36 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n stripe_customer_id AS `stripe_customer_id!`,\n user_token AS `user_token!: UserToken`\nFROM user_stripe_customer_links\nWHERE payments_namespace = 'artcraft'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_customer_id!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "2d49cfbafc29bce35f3e03135054c7521fb0182663aefa971245f91f33a6fc49"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_log_level = ?,\n maybe_creator_user_token = ?,\n maybe_ip_address = ?,\n maybe_url = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "32a8d1cfec89ba2056463a33ddc501a8a99283c7c00982a7fa79f7ff382c9b22"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE tts_results\nSET\n mod_deleted_at = CURRENT_TIMESTAMP,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "376e36b1357952599866262c7356ba35f15ccdfb338d8ccdbdc06ccd69331fc3"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_events\nSET\n token = ?,\n payments_namespace = ?,\n maybe_user_token = ?,\n event_type = ?,\n amount_usd_cents = ?,\n maybe_credits_granted = ?,\n maybe_user_subscription_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n payment_source = ?,\n source_object_id = ?,\n maybe_stripe_customer_id = ?,\n maybe_stripe_invoice_id = ?,\n maybe_stripe_payment_intent_id = ?,\n maybe_stripe_charge_id = ?,\n maybe_stripe_event_id = ?,\n is_production = ?,\n payment_occurred_at = ?\nON DUPLICATE KEY UPDATE token = token\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 17
},
"nullable": []
},
"hash": "3e5a71cedc31cc656177c8331c2de1d1d2d2eb310aabeb7e6238943b073f6998"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE tts_results\nSET\n mod_deleted_at = NULL,\n maybe_mod_user_token = ?\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "40a3301e9a3201e74d49b6b339f67291e2ae22d315188401708609ac2d28a68c"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_summaries\nSET\n payments_namespace = ?,\n user_token = ?,\n lifetime_gross_spend_usd_cents = ?,\n lifetime_subscription_spend_usd_cents = ?,\n lifetime_credits_spend_usd_cents = ?,\n lifetime_refund_usd_cents = ?,\n lifetime_net_spend_usd_cents = ?,\n lifetime_payment_count = ?,\n lifetime_refund_count = ?,\n first_payment_at = ?,\n first_spend_usd_cents = ?,\n last_payment_at = ?,\n last_spend_usd_cents = ?,\n days_since_first_payment = ?,\n days_since_last_payment = ?,\n net_spend_7d_usd_cents = ?,\n net_spend_prev_7d_usd_cents = ?,\n net_spend_14d_usd_cents = ?,\n net_spend_prev_14d_usd_cents = ?,\n net_spend_30d_usd_cents = ?,\n net_spend_prev_30d_usd_cents = ?,\n net_spend_60d_usd_cents = ?,\n net_spend_90d_usd_cents = ?,\n net_spend_this_year_usd_cents = ?,\n avg_weekly_net_spend_4w_usd_cents = ?,\n avg_weekly_net_spend_12w_usd_cents = ?,\n active_weeks_in_last_4 = ?,\n active_weeks_in_last_8 = ?,\n active_weeks_in_last_12 = ?,\n active_weeks_in_last_24 = ?,\n active_weeks_in_last_52 = ?,\n consecutive_active_weeks = ?,\n consecutive_inactive_weeks = ?,\n weeks_since_last_spend = ?,\n is_active_subscriber = ?,\n maybe_subscription_interval = ?,\n maybe_reengagement_score = ?\nON DUPLICATE KEY UPDATE\n lifetime_gross_spend_usd_cents = VALUES(lifetime_gross_spend_usd_cents),\n lifetime_subscription_spend_usd_cents = VALUES(lifetime_subscription_spend_usd_cents),\n lifetime_credits_spend_usd_cents = VALUES(lifetime_credits_spend_usd_cents),\n lifetime_refund_usd_cents = VALUES(lifetime_refund_usd_cents),\n lifetime_net_spend_usd_cents = VALUES(lifetime_net_spend_usd_cents),\n lifetime_payment_count = VALUES(lifetime_payment_count),\n lifetime_refund_count = VALUES(lifetime_refund_count),\n first_payment_at = VALUES(first_payment_at),\n first_spend_usd_cents = VALUES(first_spend_usd_cents),\n last_payment_at = VALUES(last_payment_at),\n last_spend_usd_cents = VALUES(last_spend_usd_cents),\n days_since_first_payment = VALUES(days_since_first_payment),\n days_since_last_payment = VALUES(days_since_last_payment),\n net_spend_7d_usd_cents = VALUES(net_spend_7d_usd_cents),\n net_spend_prev_7d_usd_cents = VALUES(net_spend_prev_7d_usd_cents),\n net_spend_14d_usd_cents = VALUES(net_spend_14d_usd_cents),\n net_spend_prev_14d_usd_cents = VALUES(net_spend_prev_14d_usd_cents),\n net_spend_30d_usd_cents = VALUES(net_spend_30d_usd_cents),\n net_spend_prev_30d_usd_cents = VALUES(net_spend_prev_30d_usd_cents),\n net_spend_60d_usd_cents = VALUES(net_spend_60d_usd_cents),\n net_spend_90d_usd_cents = VALUES(net_spend_90d_usd_cents),\n net_spend_this_year_usd_cents = VALUES(net_spend_this_year_usd_cents),\n avg_weekly_net_spend_4w_usd_cents = VALUES(avg_weekly_net_spend_4w_usd_cents),\n avg_weekly_net_spend_12w_usd_cents = VALUES(avg_weekly_net_spend_12w_usd_cents),\n active_weeks_in_last_4 = VALUES(active_weeks_in_last_4),\n active_weeks_in_last_8 = VALUES(active_weeks_in_last_8),\n active_weeks_in_last_12 = VALUES(active_weeks_in_last_12),\n active_weeks_in_last_24 = VALUES(active_weeks_in_last_24),\n active_weeks_in_last_52 = VALUES(active_weeks_in_last_52),\n consecutive_active_weeks = VALUES(consecutive_active_weeks),\n consecutive_inactive_weeks = VALUES(consecutive_inactive_weeks),\n weeks_since_last_spend = VALUES(weeks_since_last_spend),\n is_active_subscriber = VALUES(is_active_subscriber),\n maybe_subscription_interval = VALUES(maybe_subscription_interval),\n maybe_reengagement_score = VALUES(maybe_reengagement_score)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 37
},
"nullable": []
},
"hash": "41cf2f8d64aa36fb930cc1bf8a46d37e0987b743438b44e1c55859fc6b6ee6a0"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO voice_clone_requests\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n maybe_user_token = ?,\n email_address = ?,\n discord_username = ?,\n\n is_for_private_use = ?,\n is_for_public_use = ?,\n\n is_for_studio = ?,\n is_for_twitch_tts = ?,\n is_for_api_use = ?,\n is_for_music = ?,\n is_for_games = ?,\n is_for_other = ?,\n optional_notes_on_use = ?,\n\n is_own_voice = ?,\n is_third_party_voice = ?,\n optional_notes_on_subject = ?,\n\n has_clean_audio_recordings = ?,\n has_good_microphone = ?,\n\n optional_questions = ?,\n optional_extra_comments = ?,\n\n ip_address_creation = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 22
},
"nullable": []
},
"hash": "4cea10944b8869419ecdfea30c58f49a87c090b9083078043177c33737ecb949"
}
@@ -1,124 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.maybe_reengagement_score AS `reengagement_score!: u32`,\n s.lifetime_net_spend_usd_cents,\n s.last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n s.days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n s.weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n s.is_active_subscriber AS `is_active_subscriber: bool`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\n AND s.maybe_reengagement_score > 0\nORDER BY s.maybe_reengagement_score DESC, s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "reengagement_score!: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
false,
true,
true,
true,
false
]
},
"hash": "5b1954301fc16703a04e2f63fd85c28bf2283bc1ecb5624107686dad0dfe364f"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n COUNT(distinct token) as record_count,\n NOW() as present_time\nFROM\n(\n SELECT token\n FROM tts_inference_jobs\n WHERE status = \"started\"\n AND created_at > (CURDATE() - INTERVAL 5 MINUTE)\nUNION\n SELECT token\n FROM tts_inference_jobs\n WHERE status IN (\"pending\", \"attempt_failed\")\n) as t\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "record_count",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 1,
"name": "present_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "6098826a8ada75442be5b7ac1d0199694e01c5959666f59986cd8b2ed6f51882"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n job_type as queue_type,\n count(*) as pending_job_count,\n NOW() as present_time\n FROM (\n SELECT\n token,\n job_type\n FROM generic_inference_jobs\n WHERE status IN (\"pending\", \"attempt_failed\")\n AND job_type IS NOT NULL\n UNION\n SELECT\n token,\n job_type\n FROM generic_inference_jobs\n WHERE status IN (\"started\")\n AND created_at > (CURDATE() - INTERVAL 15 MINUTE)\n AND job_type IS NOT NULL\n) as generic_inner\nGROUP BY queue_type\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "queue_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "pending_job_count",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 2,
"name": "present_time",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
true,
false,
false
]
},
"hash": "73dc054488b2fa3efc923d3a90914a1450b73e99a377562484a0498ccce1676f"
}
@@ -1,146 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n spend_date AS `spend_date!: NaiveDate`,\n subscription_spend_usd_cents,\n credits_spend_usd_cents,\n gross_spend_usd_cents,\n refund_usd_cents,\n net_spend_usd_cents,\n payment_count,\n credits_granted,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_daily_spends\nWHERE user_token = ?\n AND payments_namespace = ?\nORDER BY spend_date DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "spend_date!: NaiveDate",
"type_info": {
"type": "Date",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "credits_granted",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 11,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "77dca63cffdd18957cbc4be6b7d982a2599a2c06df07f8e34501a032cd688ead"
}
@@ -1,113 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_user_token AS `user_token!: UserToken`,\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0\n AND event_type IN ('subscription_initial', 'subscription_renewal', 'subscription_proration_upgrade')\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `subscription_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 AND event_type = 'credit_pack_purchase'\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `credits_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents < 0 THEN -amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payment_occurred_at >= ?\n AND payment_occurred_at < ?\nGROUP BY maybe_user_token, payments_namespace\nHAVING SUM(CASE WHEN amount_usd_cents <> 0 THEN 1 ELSE 0 END) > 0\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 3,
"name": "credits_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 4,
"name": "gross_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 5,
"name": "refund_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 6,
"name": "net_spend_usd_cents!: i64",
"type_info": {
"type": "LongLong",
"flags": "BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 7,
"name": "payment_count!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 8,
"name": "credits_granted!: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
true,
true,
true,
true,
true,
true,
false
]
},
"hash": "83fc05a3594b379482745f851cd5a22786adde9968dd70490c91100cf21f8023"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO tts_model_upload_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n creator_user_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n title = ?,\n tts_model_type = ?,\n download_url = ?,\n status = \"pending\"\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "886861e91c7e8af53eb856c949389a172b8d9e079f1c2070d115423907c47c50"
}
@@ -1,102 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "user_role_slug",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "is_banned",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "can_ban_users",
"type_info": {
"type": "Tiny",
"flags": "",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "api_key_token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "9188e235caa439d50628333c2f4722b9f1a8f575c192d4d36a2039709c412ac0"
}
@@ -0,0 +1,256 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_user_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "discord_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "is_for_private_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "is_for_public_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "is_for_studio",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "is_for_twitch_tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_for_api_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_for_music",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "is_for_games",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 11,
"name": "is_for_other",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "optional_notes_on_use",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 13,
"name": "is_own_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_third_party_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "optional_notes_on_subject",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "has_clean_audio_recordings",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 17,
"name": "has_good_microphone",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 18,
"name": "optional_questions",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 19,
"name": "optional_extra_comments",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "922a7dbcfa24a2c5901d0453c229b066b3b4eea2c73fa533651bb27365f17be0"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE tts_results\nSET\n user_deleted_at = CURRENT_TIMESTAMP\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "961f792c24bee6009e3e8a215561db932871a53c48839b8cb80292de00e7b2e1"
}
@@ -1,443 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n lifetime_gross_spend_usd_cents,\n lifetime_subscription_spend_usd_cents,\n lifetime_credits_spend_usd_cents,\n lifetime_refund_usd_cents,\n lifetime_net_spend_usd_cents,\n lifetime_payment_count,\n lifetime_refund_count,\n first_payment_at AS `maybe_first_payment_at: DateTime<Utc>`,\n first_spend_usd_cents,\n last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n last_spend_usd_cents,\n days_since_first_payment AS `maybe_days_since_first_payment: u32`,\n days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n net_spend_7d_usd_cents,\n net_spend_prev_7d_usd_cents,\n net_spend_14d_usd_cents,\n net_spend_prev_14d_usd_cents,\n net_spend_30d_usd_cents,\n net_spend_prev_30d_usd_cents,\n net_spend_60d_usd_cents,\n net_spend_90d_usd_cents,\n net_spend_this_year_usd_cents,\n avg_weekly_net_spend_4w_usd_cents,\n avg_weekly_net_spend_12w_usd_cents,\n active_weeks_in_last_4,\n active_weeks_in_last_8,\n active_weeks_in_last_12,\n active_weeks_in_last_24,\n active_weeks_in_last_52,\n consecutive_active_weeks,\n consecutive_inactive_weeks,\n weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n is_active_subscriber AS `is_active_subscriber: bool`,\n maybe_subscription_interval,\n maybe_reengagement_score AS `maybe_reengagement_score: u32`,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_spend_summaries\nWHERE payments_namespace = ?\n AND user_token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "lifetime_gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 3,
"name": "lifetime_subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "lifetime_credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "lifetime_refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "lifetime_payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "lifetime_refund_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "maybe_first_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "first_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "last_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 13,
"name": "maybe_days_since_first_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 14,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 15,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 16,
"name": "net_spend_prev_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 17,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 18,
"name": "net_spend_prev_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 19,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 20,
"name": "net_spend_prev_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 21,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 22,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 23,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 24,
"name": "avg_weekly_net_spend_4w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 25,
"name": "avg_weekly_net_spend_12w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 26,
"name": "active_weeks_in_last_4",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 27,
"name": "active_weeks_in_last_8",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 28,
"name": "active_weeks_in_last_12",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 29,
"name": "active_weeks_in_last_24",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 30,
"name": "active_weeks_in_last_52",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 31,
"name": "consecutive_active_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 32,
"name": "consecutive_inactive_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 33,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 34,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 35,
"name": "maybe_subscription_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 36,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 37,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 38,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true,
false,
false
]
},
"hash": "9715e4dc2ec6e596fdb91ecc50a2b80f63494bbc0c9df6c0a57ede2360716869"
}
@@ -0,0 +1,135 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as job_token,\n\n jobs.status,\n jobs.attempt_count,\n jobs.on_success_result_token as maybe_result_token,\n results.public_bucket_wav_audio_path as maybe_public_bucket_wav_audio_path,\n\n jobs.model_token,\n tts.tts_model_type,\n tts.title,\n\n jobs.raw_inference_text,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM tts_inference_jobs as jobs\nJOIN tts_models as tts\n ON tts.token = jobs.model_token\nLEFT OUTER JOIN tts_results as results\n ON jobs.on_success_result_token = results.token\n\nWHERE jobs.token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "status",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 2,
"name": "attempt_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 3,
"name": "maybe_result_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_public_bucket_wav_audio_path",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 5,
"name": "model_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "tts_model_type",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 60
}
},
{
"ordinal": 7,
"name": "title",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 8,
"name": "raw_inference_text",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 262140
}
},
{
"ordinal": 9,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
false,
false,
false,
false,
false,
false
]
},
"hash": "a2b586dd2120f29dd26281626f7381e292c821f8e826b9bc584b92ffa45035b2"
}
@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n id as `id: u64`,\n event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n message,\n created_at\nFROM debug_logs\nWHERE event_token = ?\nORDER BY id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 5,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
false,
false
]
},
"hash": "a62b88fec40a11952cc199383090ea338f97091e8bfc6b68aad67416685889af"
}
@@ -0,0 +1,69 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.username,\n users.display_name,\n users.email_gravatar_hash as gravatar_hash,\n creator_user_token,\n count(*) as uploaded_count\nFROM tts_models\nJOIN users\nON\n users.token = creator_user_token\nWHERE\n users.is_banned IS FALSE\n AND users.token NOT IN (\"U:DRB1SKEMKHT1J\")\n AND tts_models.user_deleted_at IS NULL\n AND tts_models.mod_deleted_at IS NULL\nGROUP BY creator_user_token\nORDER BY uploaded_count desc\nLIMIT 25;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 1,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "creator_user_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "uploaded_count",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "a82fa4eb36528ebf52af2585cdda203476a4ba3645837c792830474fca2eda4f"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO debug_logs\nSET\n event_token = ?,\n debug_log_type = ?,\n maybe_creator_user_token = ?,\n message = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "a9745ecf60b564ab096ee1dbe124221b5f288a4ae38e788e19bbd359db0b580f"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_daily_spends\nSET\n user_token = ?,\n payments_namespace = ?,\n spend_date = ?,\n subscription_spend_usd_cents = ?,\n credits_spend_usd_cents = ?,\n gross_spend_usd_cents = ?,\n refund_usd_cents = ?,\n net_spend_usd_cents = ?,\n payment_count = ?,\n credits_granted = ?\nON DUPLICATE KEY UPDATE\n subscription_spend_usd_cents = VALUES(subscription_spend_usd_cents),\n credits_spend_usd_cents = VALUES(credits_spend_usd_cents),\n gross_spend_usd_cents = VALUES(gross_spend_usd_cents),\n refund_usd_cents = VALUES(refund_usd_cents),\n net_spend_usd_cents = VALUES(net_spend_usd_cents),\n payment_count = VALUES(payment_count),\n credits_granted = VALUES(credits_granted)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "b7a5fea76da70c03b8744c985940a5bcdabdb3316aa407c5ca883bca863ee633"
}
@@ -1,25 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT maybe_user_token AS `user_token!: UserToken`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payments_namespace = ?\n AND maybe_user_token > ?\nORDER BY maybe_user_token\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
true
]
},
"hash": "bcd07050d5557bf59d6b2691478763b5b067447d295b002f6e766209ea077f42"
}
@@ -1,113 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n e.maybe_user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents < 0 THEN -e.amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(e.amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN e.amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(e.maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events e\nJOIN users u ON u.token = e.maybe_user_token\nWHERE e.maybe_user_token IS NOT NULL\n AND e.is_production = TRUE\n AND e.payment_occurred_at >= ?\n AND (? IS NULL OR e.payments_namespace = ?)\nGROUP BY e.maybe_user_token, u.username, u.display_name, u.email_gravatar_hash\nORDER BY SUM(e.amount_usd_cents) DESC, e.maybe_user_token ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "gross_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 5,
"name": "refund_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 6,
"name": "net_spend_usd_cents!: i64",
"type_info": {
"type": "LongLong",
"flags": "BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 7,
"name": "payment_count!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 8,
"name": "credits_granted!: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 5
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
false
]
},
"hash": "c2ee41eb6bc1a6a551411998155423142411010f298f0815eacc7d9c3138bdd8"
}
@@ -1,91 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "user_role_slug",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "is_banned",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "can_ban_users",
"type_info": {
"type": "Tiny",
"flags": "",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "c77126adb36f9e2f0248720eb4d8c1cd9d14719f64a02678d8021e26addd2c18"
}
@@ -1,25 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT payments_namespace AS `payments_namespace!: PaymentsNamespace`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "cf8c702126bba73cd3abc2ed7202962b6da773460505222a75408dab1d26b70f"
}
@@ -1,146 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.maybe_creator_user_token = ?\n AND d.id < ?\nORDER BY d.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "maybe_url",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "d1ade7fcfd3efac1cd3163c392110327bb73edb74fa0d9693fee5feddb8a3223"
}
@@ -1,212 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n e.token AS `token!: UserSpendEventToken`,\n e.payments_namespace AS `payments_namespace!`,\n e.maybe_user_token AS `maybe_user_token: UserToken`,\n u.username AS `maybe_username?`,\n u.display_name AS `maybe_display_name?`,\n u.email_gravatar_hash AS `maybe_email_gravatar_hash?`,\n e.event_type AS `event_type!`,\n e.amount_usd_cents,\n e.maybe_credits_granted AS `maybe_credits_granted: u32`,\n e.payment_source AS `payment_source!`,\n e.source_object_id AS `maybe_source_object_id`,\n e.maybe_stripe_invoice_id,\n e.maybe_stripe_payment_intent_id,\n e.maybe_stripe_charge_id,\n e.maybe_stripe_customer_id,\n e.is_production AS `is_production: bool`,\n e.payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n e.created_at AS `created_at!: DateTime<Utc>`\nFROM user_spend_events e\nLEFT JOIN users u ON u.token = e.maybe_user_token\nWHERE (? IS NULL OR e.payments_namespace = ?)\nORDER BY e.payment_occurred_at DESC, e.id DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token!: UserSpendEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "maybe_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 4,
"name": "maybe_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 5,
"name": "maybe_email_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "event_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "payment_source!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "maybe_source_object_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 11,
"name": "maybe_stripe_invoice_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 12,
"name": "maybe_stripe_payment_intent_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 13,
"name": "maybe_stripe_charge_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 14,
"name": "maybe_stripe_customer_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 15,
"name": "is_production: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 16,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 17,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "d5d3a59ea4885868b26168cd305f6168a1e62fe9f8fa663063591335c645b927"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE tts_results\nSET\n user_deleted_at = NULL\nWHERE\n token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "dbce51eb1c80dded902c9509e50713483c7131f115e8ec10646e99e88110278b"
}
@@ -1,80 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n wle.maybe_entity_ref AS `stripe_object_id!`,\n wle.entry_type AS `entry_type!`,\n wle.token AS `ledger_token!: WalletLedgerEntryToken`,\n w.owner_user_token AS `owner_user_token!: UserToken`,\n wle.credits_delta AS `credits_delta!`,\n wle.created_at AS `created_at!: DateTime<Utc>`\nFROM wallet_ledger_entries wle\nJOIN wallets w ON w.token = wle.wallet_token\nWHERE w.wallet_namespace = 'artcraft'\n AND wle.entry_type IN ('credit_banked', 'credit_monthly')\n AND wle.created_at >= ?\n AND (wle.maybe_entity_ref LIKE 'pi\\_%' OR wle.maybe_entity_ref LIKE 'in\\_%')\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_object_id!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "entry_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "ledger_token!: WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "owner_user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "credits_delta!",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 5,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
false,
false
]
},
"hash": "de69f3d564830f8fff90f754d13982de623fcda50118d261466eeaf034de5ecb"
}
@@ -0,0 +1,256 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token,\n\n maybe_user_token,\n email_address,\n discord_username,\n\n is_for_private_use,\n is_for_public_use,\n\n is_for_studio,\n is_for_twitch_tts,\n is_for_api_use,\n is_for_music,\n is_for_games,\n is_for_other,\n optional_notes_on_use,\n\n is_own_voice,\n is_third_party_voice,\n optional_notes_on_subject,\n\n has_clean_audio_recordings,\n has_good_microphone,\n\n optional_questions,\n optional_extra_comments,\n\n created_at,\n updated_at\nFROM voice_clone_requests\nWHERE\n maybe_user_token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_user_token",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "email_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "discord_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 4,
"name": "is_for_private_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 5,
"name": "is_for_public_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 6,
"name": "is_for_studio",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "is_for_twitch_tts",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 8,
"name": "is_for_api_use",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 9,
"name": "is_for_music",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 10,
"name": "is_for_games",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 11,
"name": "is_for_other",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "optional_notes_on_use",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 13,
"name": "is_own_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_third_party_voice",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "optional_notes_on_subject",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "has_clean_audio_recordings",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 17,
"name": "has_good_microphone",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 18,
"name": "optional_questions",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 19,
"name": "optional_extra_comments",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "e5ef2bda28221a570da6dcfa73e76cae9910a0b4ee53005a87176ecf1be73dd7"
}
@@ -1,146 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n d.id as `id: u64`,\n d.event_token as `event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n d.debug_log_type as `debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType`,\n d.maybe_log_level as `maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel`,\n d.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n d.maybe_ip_address,\n d.maybe_url,\n d.message,\n d.created_at,\n u.display_name as `maybe_user_display_name?`,\n u.username as `maybe_user_username?`,\n u.email_gravatar_hash as `maybe_user_gravatar_hash?`\nFROM debug_logs d\nLEFT OUTER JOIN users u\n ON u.token = d.maybe_creator_user_token\nWHERE d.event_token = ?\nORDER BY d.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "debug_log_type: enums::by_table::debug_logs::debug_log_type::DebugLogType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 96
}
},
{
"ordinal": 3,
"name": "maybe_log_level: enums::by_table::debug_logs::debug_log_level::DebugLogLevel",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "maybe_url",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 7,
"name": "message",
"type_info": {
"type": "Blob",
"flags": "NOT_NULL | BLOB | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 10,
"name": "maybe_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false,
false,
true,
true,
true
]
},
"hash": "e9a149ffc963a59019fdd69bd53128fad946856d432108bf9b923dfeaf2442c5"
}
@@ -1,157 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.lifetime_net_spend_usd_cents,\n s.net_spend_7d_usd_cents,\n s.net_spend_14d_usd_cents,\n s.net_spend_30d_usd_cents,\n s.net_spend_60d_usd_cents,\n s.net_spend_90d_usd_cents,\n s.net_spend_this_year_usd_cents,\n s.is_active_subscriber AS `is_active_subscriber: bool`,\n s.maybe_reengagement_score AS `maybe_reengagement_score: u32`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\nORDER BY\n (CASE ?\n WHEN '7d' THEN s.net_spend_7d_usd_cents\n WHEN '14d' THEN s.net_spend_14d_usd_cents\n WHEN '30d' THEN s.net_spend_30d_usd_cents\n WHEN '60d' THEN s.net_spend_60d_usd_cents\n WHEN '90d' THEN s.net_spend_90d_usd_cents\n WHEN '1y' THEN s.net_spend_this_year_usd_cents\n ELSE CAST(s.lifetime_net_spend_usd_cents AS SIGNED)\n END) DESC,\n s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 9,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "f6db2f3bf8695fa8431db209576328154b8ac20aa6e899739095b6206bb38944"
}
@@ -1,47 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_stripe_subscription_status,\n maybe_stripe_recurring_interval,\n subscription_expires_at AS `subscription_expires_at!: DateTime<Utc>`\nFROM user_subscriptions\nWHERE user_token = ?\n AND subscription_namespace = ?\nORDER BY subscription_expires_at DESC\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_stripe_subscription_status",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_stripe_recurring_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_expires_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true,
false
]
},
"hash": "f74615faa4321ec4d583829997e12cb8ae152afefd2574385c120c63278893d2"
}
Generated
-47
View File
@@ -3049,8 +3049,6 @@ dependencies = [
"futures 0.3.32",
"log",
"mysql_queries",
"reqwest 0.12.23",
"rustls 0.23.36",
"serde",
"serde_derive",
"serde_json",
@@ -3621,7 +3619,6 @@ dependencies = [
"env_logger 0.11.9",
"log",
"storyteller_root",
"trace_id",
"workspace-build-acceleration",
]
@@ -6767,7 +6764,6 @@ version = "0.0.1"
dependencies = [
"env_logger 0.11.9",
"log",
"trace_id",
"workspace-build-acceleration",
]
@@ -8288,7 +8284,6 @@ dependencies = [
"rootly_client",
"sha2 0.11.0-rc.5",
"tokio",
"trace_id",
"workspace-build-acceleration",
]
@@ -11681,7 +11676,6 @@ dependencies = [
"tokens",
"tokio",
"toml 1.0.7+spec-1.1.0",
"trace_id",
"tts_common",
"url",
"url_config",
@@ -13320,17 +13314,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "trace_id"
version = "0.0.1"
dependencies = [
"rand 0.8.5",
"serde",
"serde_derive",
"tokio",
"workspace-build-acceleration",
]
[[package]]
name = "tracing"
version = "0.1.41"
@@ -13719,36 +13702,6 @@ dependencies = [
"url",
]
[[package]]
name = "user-spend-analytics-job"
version = "0.0.1"
dependencies = [
"actix-web",
"actix_helpers",
"anyhow",
"bootstrap",
"chrono",
"concurrency",
"easyenv",
"enums",
"errors",
"jobs_common",
"log",
"mysql_queries",
"pager",
"rootly_client",
"rootly_config",
"serde",
"serde_derive",
"serde_json",
"server_environment",
"shared_env_var_config",
"sqlx",
"tokens",
"tokio",
"workspace-build-acceleration",
]
[[package]]
name = "user_input_common"
version = "0.0.1"
-3
View File
@@ -130,14 +130,12 @@ members = [
"crates/schema/public/enums",
"crates/schema/public/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/schema/public/tokens",
"crates/schema/trace_id",
# Services: API servers, jobs, workers, etc.
"crates/service/job/gmicloud_job",
"crates/service/job/grok_api_job",
"crates/service/job/seedance2_pro_job",
"crates/service/job/video_thumbnail_job",
"crates/service/job/user_spend_analytics_job",
"crates/service/job/worldlabs_job",
"crates/service/web/dummy_service",
"crates/service/web/storyteller_web",
@@ -294,7 +292,6 @@ subprocess_common = { path = "crates/lib/deprecated/subprocess_common" }
test_data = { path = "crates/testing/test_data" }
test_utils = { path = "crates/testing/test_utils" }
tokens = { path = "crates/schema/public/tokens" }
trace_id = { path = "crates/schema/trace_id" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
uuid_utils = { path = "crates/lib/uuid_utils" }
@@ -1,5 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_events;
@@ -1,103 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,5 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_daily_spends;
@@ -1,44 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,5 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_summaries;
@@ -1,95 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_log_level;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_log_level VARCHAR(16) DEFAULT NULL;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP INDEX index_maybe_log_level;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD INDEX index_maybe_log_level (maybe_log_level);
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_ip_address;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_ip_address VARCHAR(40) DEFAULT NULL;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
DROP COLUMN maybe_url;
@@ -1,6 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE debug_logs
ADD COLUMN maybe_url VARCHAR(255) DEFAULT NULL;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
DROP INDEX idx_creator_created_at,
ALGORITHM=INPLACE, LOCK=NONE;
@@ -1,17 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Composite index for the library / user profile list queries:
-- WHERE maybe_creator_user_token = ? ... ORDER BY created_at DESC LIMIT n
--
-- Without this, the optimizer picks an index-merge intersection against huge
-- low-selectivity indexes (e.g. index_user_deleted_at, ~4.75M NULL entries),
-- scanning millions of index records per page load.
--
-- NB: ADD INDEX is online DDL (ALGORITHM=INPLACE, LOCK=NONE) — reads and writes
-- continue during the build. Expect a few minutes of elevated I/O on a table
-- this size (~8.5M rows).
ALTER TABLE media_files
ADD INDEX idx_creator_created_at (maybe_creator_user_token, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
@@ -13,18 +13,9 @@ CREATE TABLE debug_logs (
-- The type of debug log entry.
debug_log_type VARCHAR(24) NOT NULL,
-- The severity level of the log entry (info, warn, error, debug, trace).
maybe_log_level VARCHAR(16) DEFAULT NULL,
-- The user who created the log entry (if any).
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
-- The client IP address of the request (if any). Wide enough for IPv6.
maybe_ip_address VARCHAR(40) DEFAULT NULL,
-- The request URL (if any). Truncated to 255 characters on insert.
maybe_url VARCHAR(255) DEFAULT NULL,
-- The log message body.
message MEDIUMTEXT NOT NULL,
@@ -34,7 +25,6 @@ CREATE TABLE debug_logs (
PRIMARY KEY (id),
KEY index_event_token (event_token),
KEY index_debug_log_type (debug_log_type),
KEY index_maybe_log_level (maybe_log_level),
KEY index_maybe_creator_user_token (maybe_creator_user_token),
KEY index_created_at (created_at)
@@ -364,7 +364,6 @@ CREATE TABLE media_files (
KEY index_user_deleted_at (user_deleted_at),
KEY index_mod_deleted_at (mod_deleted_at),
KEY index_media_files_created_at (created_at),
KEY index_media_files_updated_at (updated_at),
KEY idx_creator_created_at (maybe_creator_user_token, created_at)
KEY index_media_files_updated_at (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,47 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,106 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -1,98 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-2
View File
@@ -138,7 +138,6 @@ RUN SQLX_OFFLINE=true \
$HOME/.cargo/bin/cargo build \
--release \
--bin storyteller-web \
--bin user-spend-analytics-job \
--bin seedance2-pro-job \
--bin worldlabs-job \
--bin video-thumbnail-job \
@@ -191,7 +190,6 @@ COPY --from=builder /tmp/target/release/analytics-job /
COPY --from=builder /tmp/target/release/email-sender-job /
COPY --from=builder /tmp/target/release/es-update-job /
COPY --from=builder /tmp/target/release/grok-api-job /
COPY --from=builder /tmp/target/release/user-spend-analytics-job /
# Legacy apps:
# COPY --from=builder /tmp/target/release/tts-download-job /
@@ -1,36 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::Serialize;
use utoipa::ToSchema;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
/// A single debug log row.
/// Shared by the moderation debug-log list endpoints.
#[derive(Serialize, ToSchema)]
pub struct ModerationDebugLogEntry {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
/// The client IP address of the request (if recorded).
pub maybe_ip_address: Option<String>,
/// The request URL (if recorded; truncated to 255 chars).
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
/// The creator user, joined in when the row has a user token.
pub maybe_user: Option<ModerationDebugLogUser>,
}
/// The creator user of a debug log row.
#[derive(Serialize, ToSchema)]
pub struct ModerationDebugLogUser {
pub user_token: UserToken,
pub display_name: String,
pub username: String,
pub gravatar_hash: String,
}
@@ -1,4 +0,0 @@
pub mod debug_log_entry;
pub mod moderation_list_all_debug_logs;
pub mod moderation_list_debug_logs_for_token;
pub mod moderation_list_debug_logs_for_user;
@@ -1,76 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use enums::error::enum_error::EnumError;
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogUser;
pub const MODERATION_LIST_ALL_DEBUG_LOGS_PATH: &str = "/v1/moderation/debug_logs/list_all";
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListAllDebugLogsQueryParams {
/// Optional comma-separated list of severity levels to include,
/// eg. `severities=error,warn`. Omit to include all rows.
pub severities: Option<String>,
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
pub cursor: Option<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
impl ModerationListAllDebugLogsQueryParams {
/// Parse the comma-separated `severities` parameter.
/// Returns `None` when the parameter is absent or empty.
pub fn parsed_severities(&self) -> Result<Option<Vec<DebugLogLevel>>, EnumError> {
let raw = match self.severities.as_deref() {
Some(value) if !value.trim().is_empty() => value,
_ => return Ok(None),
};
let levels = raw
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(DebugLogLevel::from_str)
.collect::<Result<Vec<DebugLogLevel>, EnumError>>()?;
Ok(if levels.is_empty() { None } else { Some(levels) })
}
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListAllDebugLogsSuccessResponse {
pub success: bool,
/// Most recent first.
pub debug_logs: Vec<ModerationListAllDebugLogsEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
/// A debug log row with the creator user joined in (when one exists).
#[derive(Serialize, ToSchema)]
pub struct ModerationListAllDebugLogsEntry {
pub id: u64,
pub event_token: DebugLogEventToken,
pub debug_log_type: DebugLogType,
pub maybe_log_level: Option<DebugLogLevel>,
pub maybe_creator_user_token: Option<UserToken>,
/// The client IP address of the request (if recorded).
pub maybe_ip_address: Option<String>,
/// The request URL (if recorded; truncated to 255 chars).
pub maybe_url: Option<String>,
pub message: String,
pub created_at: DateTime<Utc>,
pub maybe_user: Option<ModerationDebugLogUser>,
}
@@ -1,30 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
pub const MODERATION_LIST_DEBUG_LOGS_FOR_TOKEN_PATH: &str = "/v1/moderation/debug_logs/list/{token}";
// ── Path params ──
#[derive(Deserialize, ToSchema)]
pub struct ModerationListDebugLogsForTokenPathInfo {
pub token: DebugLogEventToken,
}
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListDebugLogsForTokenQueryParams {
pub limit: Option<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForTokenSuccessResponse {
pub success: bool,
pub debug_logs: Vec<ModerationDebugLogEntry>,
}
@@ -1,37 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::users::UserToken;
use crate::moderation::debug_logs::debug_log_entry::ModerationDebugLogEntry;
pub const MODERATION_LIST_DEBUG_LOGS_FOR_USER_PATH: &str = "/v1/moderation/debug_logs/user_list/{user_token}";
// ── Path params ──
#[derive(Deserialize, ToSchema)]
pub struct ModerationListDebugLogsForUserPathInfo {
pub user_token: UserToken,
}
// ── Query params ──
#[derive(Deserialize, IntoParams, ToSchema)]
pub struct ModerationListDebugLogsForUserQueryParams {
/// Cursor for pagination. Pass the `next_cursor` from a previous response.
pub cursor: Option<u64>,
/// Max rows per page (default 50, max 200).
pub limit: Option<u32>,
}
// ── Response ──
#[derive(Serialize, ToSchema)]
pub struct ModerationListDebugLogsForUserSuccessResponse {
pub success: bool,
/// Most recent first.
pub debug_logs: Vec<ModerationDebugLogEntry>,
/// Cursor for the next page. `None` if there are no more results.
pub next_cursor: Option<u64>,
}
@@ -1,12 +1,7 @@
pub mod alerts;
pub mod debug_logs;
pub mod top_spenders;
pub mod jobs;
pub mod user;
pub mod user_referrals;
pub mod user_stripe_data;
pub mod wallet_ledger_entries;
pub mod wallets;
pub mod user_daily_spends;
pub mod user_spend_summaries;
pub mod user_spend_events;
@@ -1,82 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_LIST_TOP_SPENDERS_PATH: &str =
"/v1/moderation/top_spenders/list";
/// Rolling aggregation window for the top spenders list.
#[derive(Deserialize, Serialize, Clone, Copy, ToSchema)]
pub enum TopSpendersWindow {
#[serde(rename = "24h")]
TwentyFourHours,
#[serde(rename = "7d")]
SevenDays,
#[serde(rename = "14d")]
FourteenDays,
#[serde(rename = "30d")]
ThirtyDays,
#[serde(rename = "60d")]
SixtyDays,
#[serde(rename = "90d")]
NinetyDays,
#[serde(rename = "180d")]
OneHundredEightyDays,
#[serde(rename = "365d")]
ThreeHundredSixtyFiveDays,
}
impl TopSpendersWindow {
/// Window length in hours.
pub fn as_hours(self) -> i64 {
match self {
TopSpendersWindow::TwentyFourHours => 24,
TopSpendersWindow::SevenDays => 7 * 24,
TopSpendersWindow::FourteenDays => 14 * 24,
TopSpendersWindow::ThirtyDays => 30 * 24,
TopSpendersWindow::SixtyDays => 60 * 24,
TopSpendersWindow::NinetyDays => 90 * 24,
TopSpendersWindow::OneHundredEightyDays => 180 * 24,
TopSpendersWindow::ThreeHundredSixtyFiveDays => 365 * 24,
}
}
}
#[derive(Deserialize, IntoParams)]
pub struct ModeratorListTopSpendersQueryParams {
/// Aggregation window: `24h`, `7d`, `14d`, `30d` (default), `60d`, `90d`,
/// `180d`, or `365d`.
pub window: Option<TopSpendersWindow>,
/// Row offset for pagination (page size is fixed at 100). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Optional namespace filter (e.g. `artcraft`). Omit to return all namespaces.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorListTopSpendersResponse {
pub success: bool,
/// The window the results were aggregated over (echoed back, post-default).
pub window: TopSpendersWindow,
pub spenders: Vec<TopSpenderEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct TopSpenderEntry {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
/// Sum of positive amounts in the window.
pub gross_spend_usd_cents: u64,
/// Refund/chargeback magnitude in the window (positive number).
pub refund_usd_cents: u64,
/// Gross minus refunds. Can be negative if refunds outweigh purchases.
pub net_spend_usd_cents: i64,
/// Number of positive money events in the window.
pub payment_count: u64,
/// Credits granted in the window.
pub credits_granted: u64,
}
@@ -1 +0,0 @@
pub mod list;
@@ -1 +0,0 @@
pub mod user_daily_spends_list;
@@ -1,41 +0,0 @@
use chrono::{DateTime, NaiveDate, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_USER_DAILY_SPENDS_PATH: &str =
"/v1/moderation/user_daily_spends/user/{user_token}";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorUserDailySpendsQueryParams {
/// Page size (default 200, max 5000).
pub limit: Option<u32>,
/// Row offset for pagination. Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserDailySpendsResponse {
pub success: bool,
pub user_token: UserToken,
pub records: Vec<UserDailySpendEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct UserDailySpendEntry {
pub payments_namespace: String,
pub spend_date: NaiveDate,
pub subscription_spend_usd_cents: u64,
pub credits_spend_usd_cents: u64,
pub gross_spend_usd_cents: u64,
pub refund_usd_cents: u64,
pub net_spend_usd_cents: i64,
pub payment_count: u32,
pub credits_granted: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -1,46 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::user_spend_events::UserSpendEventToken;
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_LIST_USER_SPEND_EVENTS_PATH: &str =
"/v1/moderation/user_spend_events/list";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorListUserSpendEventsQueryParams {
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Optional namespace filter (e.g. `artcraft`). Omit to return all namespaces.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorListUserSpendEventsResponse {
pub success: bool,
pub events: Vec<UserSpendEventListEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct UserSpendEventListEntry {
pub token: UserSpendEventToken,
pub payments_namespace: String,
pub maybe_user_token: Option<UserToken>,
pub maybe_username: Option<String>,
pub maybe_display_name: Option<String>,
pub maybe_email_gravatar_hash: Option<String>,
pub event_type: String,
pub amount_usd_cents: i64,
pub maybe_credits_granted: Option<u32>,
pub payment_source: String,
pub maybe_source_object_id: Option<String>,
pub maybe_stripe_invoice_id: Option<String>,
pub maybe_stripe_payment_intent_id: Option<String>,
pub maybe_stripe_charge_id: Option<String>,
pub maybe_stripe_customer_id: Option<String>,
pub is_production: bool,
pub payment_occurred_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
@@ -1,69 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_GET_USER_SPEND_SUMMARY_PATH: &str =
"/v1/moderation/user_spend_summaries/summary/{user_token}";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorGetUserSpendSummaryQueryParams {
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetUserSpendSummaryResponse {
pub success: bool,
/// `None` if the user has no spend summary row.
pub maybe_summary: Option<UserSpendSummaryView>,
}
/// The full `user_spend_summaries` record for a user.
#[derive(Serialize, ToSchema)]
pub struct UserSpendSummaryView {
pub payments_namespace: String,
pub user_token: UserToken,
pub lifetime_gross_spend_usd_cents: u64,
pub lifetime_subscription_spend_usd_cents: u64,
pub lifetime_credits_spend_usd_cents: u64,
pub lifetime_refund_usd_cents: u64,
pub lifetime_net_spend_usd_cents: u64,
pub lifetime_payment_count: u32,
pub lifetime_refund_count: u32,
pub maybe_first_payment_at: Option<DateTime<Utc>>,
pub first_spend_usd_cents: u64,
pub maybe_last_payment_at: Option<DateTime<Utc>>,
pub last_spend_usd_cents: u64,
pub maybe_days_since_first_payment: Option<u32>,
pub maybe_days_since_last_payment: Option<u32>,
pub net_spend_7d_usd_cents: i64,
pub net_spend_prev_7d_usd_cents: i64,
pub net_spend_14d_usd_cents: i64,
pub net_spend_prev_14d_usd_cents: i64,
pub net_spend_30d_usd_cents: i64,
pub net_spend_prev_30d_usd_cents: i64,
pub net_spend_60d_usd_cents: i64,
pub net_spend_90d_usd_cents: i64,
pub net_spend_this_year_usd_cents: i64,
pub avg_weekly_net_spend_4w_usd_cents: i64,
pub avg_weekly_net_spend_12w_usd_cents: i64,
pub active_weeks_in_last_4: u8,
pub active_weeks_in_last_8: u8,
pub active_weeks_in_last_12: u8,
pub active_weeks_in_last_24: u8,
pub active_weeks_in_last_52: u8,
pub consecutive_active_weeks: u32,
pub consecutive_inactive_weeks: u32,
pub maybe_weeks_since_last_spend: Option<u32>,
pub is_active_subscriber: bool,
pub maybe_subscription_interval: Option<String>,
pub maybe_reengagement_score: Option<u32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -1,3 +0,0 @@
pub mod get_summary;
pub mod reengagement_list;
pub mod top_users_list;
@@ -1,37 +0,0 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_REENGAGEMENT_LIST_PATH: &str =
"/v1/moderation/user_spend_summaries/reengagement_list";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorReengagementListQueryParams {
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorReengagementListResponse {
pub success: bool,
pub candidates: Vec<ReengagementCandidateEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct ReengagementCandidateEntry {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
pub reengagement_score: u32,
pub lifetime_net_spend_usd_cents: u64,
pub maybe_last_payment_at: Option<DateTime<Utc>>,
pub maybe_days_since_last_payment: Option<u32>,
pub maybe_weeks_since_last_spend: Option<u32>,
pub is_active_subscriber: bool,
}
@@ -1,77 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_TOP_USERS_LIST_PATH: &str =
"/v1/moderation/user_spend_summaries/top_users_list";
/// Net-spend window that selects which column `top_users_list` sorts by.
#[derive(Deserialize, Serialize, Clone, Copy, ToSchema)]
pub enum TopUsersWindow {
#[serde(rename = "7d")]
SevenDays,
#[serde(rename = "14d")]
FourteenDays,
#[serde(rename = "30d")]
ThirtyDays,
#[serde(rename = "60d")]
SixtyDays,
#[serde(rename = "90d")]
NinetyDays,
#[serde(rename = "1y")]
OneYear,
#[serde(rename = "lifetime")]
Lifetime,
}
impl TopUsersWindow {
/// The `window` key the `list_top_users_by_net_spend` query matches on.
pub fn as_query_key(self) -> &'static str {
match self {
TopUsersWindow::SevenDays => "7d",
TopUsersWindow::FourteenDays => "14d",
TopUsersWindow::ThirtyDays => "30d",
TopUsersWindow::SixtyDays => "60d",
TopUsersWindow::NinetyDays => "90d",
TopUsersWindow::OneYear => "1y",
TopUsersWindow::Lifetime => "lifetime",
}
}
}
#[derive(Deserialize, IntoParams)]
pub struct ModeratorTopUsersListQueryParams {
/// Sort window: `7d`, `14d`, `30d`, `60d`, `90d`, `1y`, or `lifetime` (default).
pub window: Option<TopUsersWindow>,
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorTopUsersListResponse {
pub success: bool,
/// The window that was sorted by (echo of the request).
pub window: String,
pub users: Vec<TopUserEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct TopUserEntry {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
pub lifetime_net_spend_usd_cents: u64,
pub net_spend_7d_usd_cents: i64,
pub net_spend_14d_usd_cents: i64,
pub net_spend_30d_usd_cents: i64,
pub net_spend_60d_usd_cents: i64,
pub net_spend_90d_usd_cents: i64,
pub net_spend_this_year_usd_cents: i64,
pub is_active_subscriber: bool,
pub maybe_reengagement_score: Option<u32>,
}
@@ -1,131 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonAspectRatio` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientAspectRatio {
Auto,
Square,
WideThreeByTwo,
WideFourByThree,
WideFiveByFour,
WideSixteenByNine,
WideTwentyOneByNine,
TallTwoByThree,
TallThreeByFour,
TallFourByFive,
TallNineBySixteen,
TallNineByTwentyOne,
Wide,
Tall,
Auto2k,
Auto3k,
Auto4k,
SquareHd,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientAspectRatio {
fn from(value: String) -> Self {
match value.as_str() {
"auto" => Self::Auto,
"square" => Self::Square,
"wide_three_by_two" => Self::WideThreeByTwo,
"wide_four_by_three" => Self::WideFourByThree,
"wide_five_by_four" => Self::WideFiveByFour,
"wide_sixteen_by_nine" => Self::WideSixteenByNine,
"wide_twenty_one_by_nine" => Self::WideTwentyOneByNine,
"tall_two_by_three" => Self::TallTwoByThree,
"tall_three_by_four" => Self::TallThreeByFour,
"tall_four_by_five" => Self::TallFourByFive,
"tall_nine_by_sixteen" => Self::TallNineBySixteen,
"tall_nine_by_twenty_one" => Self::TallNineByTwentyOne,
"wide" => Self::Wide,
"tall" => Self::Tall,
"auto_2k" => Self::Auto2k,
"auto_3k" => Self::Auto3k,
"auto_4k" => Self::Auto4k,
"square_hd" => Self::SquareHd,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientAspectRatio> for String {
fn from(value: ApiClientAspectRatio) -> Self {
match value {
ApiClientAspectRatio::Auto => "auto".to_string(),
ApiClientAspectRatio::Square => "square".to_string(),
ApiClientAspectRatio::WideThreeByTwo => "wide_three_by_two".to_string(),
ApiClientAspectRatio::WideFourByThree => "wide_four_by_three".to_string(),
ApiClientAspectRatio::WideFiveByFour => "wide_five_by_four".to_string(),
ApiClientAspectRatio::WideSixteenByNine => "wide_sixteen_by_nine".to_string(),
ApiClientAspectRatio::WideTwentyOneByNine => "wide_twenty_one_by_nine".to_string(),
ApiClientAspectRatio::TallTwoByThree => "tall_two_by_three".to_string(),
ApiClientAspectRatio::TallThreeByFour => "tall_three_by_four".to_string(),
ApiClientAspectRatio::TallFourByFive => "tall_four_by_five".to_string(),
ApiClientAspectRatio::TallNineBySixteen => "tall_nine_by_sixteen".to_string(),
ApiClientAspectRatio::TallNineByTwentyOne => "tall_nine_by_twenty_one".to_string(),
ApiClientAspectRatio::Wide => "wide".to_string(),
ApiClientAspectRatio::Tall => "tall".to_string(),
ApiClientAspectRatio::Auto2k => "auto_2k".to_string(),
ApiClientAspectRatio::Auto3k => "auto_3k".to_string(),
ApiClientAspectRatio::Auto4k => "auto_4k".to_string(),
ApiClientAspectRatio::SquareHd => "square_hd".to_string(),
ApiClientAspectRatio::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientAspectRatio = serde_json::from_str("\"auto\"").unwrap();
assert_eq!(parsed, ApiClientAspectRatio::Auto);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"auto\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientAspectRatio = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientAspectRatio::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientAspectRatio::Auto, "auto"),
(ApiClientAspectRatio::Square, "square"),
(ApiClientAspectRatio::WideThreeByTwo, "wide_three_by_two"),
(ApiClientAspectRatio::WideFourByThree, "wide_four_by_three"),
(ApiClientAspectRatio::WideFiveByFour, "wide_five_by_four"),
(ApiClientAspectRatio::WideSixteenByNine, "wide_sixteen_by_nine"),
(ApiClientAspectRatio::WideTwentyOneByNine, "wide_twenty_one_by_nine"),
(ApiClientAspectRatio::TallTwoByThree, "tall_two_by_three"),
(ApiClientAspectRatio::TallThreeByFour, "tall_three_by_four"),
(ApiClientAspectRatio::TallFourByFive, "tall_four_by_five"),
(ApiClientAspectRatio::TallNineBySixteen, "tall_nine_by_sixteen"),
(ApiClientAspectRatio::TallNineByTwentyOne, "tall_nine_by_twenty_one"),
(ApiClientAspectRatio::Wide, "wide"),
(ApiClientAspectRatio::Tall, "tall"),
(ApiClientAspectRatio::Auto2k, "auto_2k"),
(ApiClientAspectRatio::Auto3k, "auto_3k"),
(ApiClientAspectRatio::Auto4k, "auto_4k"),
(ApiClientAspectRatio::SquareHd, "square_hd"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientAspectRatio = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,67 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonBitrate` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientBitrate {
Normal,
High,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientBitrate {
fn from(value: String) -> Self {
match value.as_str() {
"normal" => Self::Normal,
"high" => Self::High,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientBitrate> for String {
fn from(value: ApiClientBitrate) -> Self {
match value {
ApiClientBitrate::Normal => "normal".to_string(),
ApiClientBitrate::High => "high".to_string(),
ApiClientBitrate::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientBitrate = serde_json::from_str("\"normal\"").unwrap();
assert_eq!(parsed, ApiClientBitrate::Normal);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"normal\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientBitrate = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientBitrate::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientBitrate::Normal, "normal"),
(ApiClientBitrate::High, "high"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientBitrate = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,83 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `GenerationProvider` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientGenerationProvider {
Artcraft,
Fal,
Grok,
Midjourney,
Sora,
WorldLabs,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientGenerationProvider {
fn from(value: String) -> Self {
match value.as_str() {
"artcraft" => Self::Artcraft,
"fal" => Self::Fal,
"grok" => Self::Grok,
"midjourney" => Self::Midjourney,
"sora" => Self::Sora,
"world_labs" => Self::WorldLabs,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientGenerationProvider> for String {
fn from(value: ApiClientGenerationProvider) -> Self {
match value {
ApiClientGenerationProvider::Artcraft => "artcraft".to_string(),
ApiClientGenerationProvider::Fal => "fal".to_string(),
ApiClientGenerationProvider::Grok => "grok".to_string(),
ApiClientGenerationProvider::Midjourney => "midjourney".to_string(),
ApiClientGenerationProvider::Sora => "sora".to_string(),
ApiClientGenerationProvider::WorldLabs => "world_labs".to_string(),
ApiClientGenerationProvider::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"artcraft\"").unwrap();
assert_eq!(parsed, ApiClientGenerationProvider::Artcraft);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"artcraft\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientGenerationProvider::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientGenerationProvider::Artcraft, "artcraft"),
(ApiClientGenerationProvider::Fal, "fal"),
(ApiClientGenerationProvider::Grok, "grok"),
(ApiClientGenerationProvider::Midjourney, "midjourney"),
(ApiClientGenerationProvider::Sora, "sora"),
(ApiClientGenerationProvider::WorldLabs, "world_labs"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientGenerationProvider = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,139 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonImageModel` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientImageModel {
Flux1Dev,
Flux1Schnell,
FluxPro11,
FluxPro11Ultra,
GptImage1,
GptImage1p5,
GptImage2,
GrokImagineImage,
GrokImagineImageQuality,
Midjourney7,
Midjourney7Niji,
Midjourney8,
NanoBanana,
NanoBanana2,
NanoBananaPro,
Seedream4,
Seedream4p5,
Seedream5Lite,
QwenEdit2511Angles,
Flux2LoraAngles,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientImageModel {
fn from(value: String) -> Self {
match value.as_str() {
"flux_1_dev" => Self::Flux1Dev,
"flux_1_schnell" => Self::Flux1Schnell,
"flux_pro_1p1" => Self::FluxPro11,
"flux_pro_1p1_ultra" => Self::FluxPro11Ultra,
"gpt_image_1" => Self::GptImage1,
"gpt_image_1p5" => Self::GptImage1p5,
"gpt_image_2" => Self::GptImage2,
"grok_imagine_image" => Self::GrokImagineImage,
"grok_imagine_image_q" => Self::GrokImagineImageQuality,
"midjourney_7" => Self::Midjourney7,
"midjourney_7_niji" => Self::Midjourney7Niji,
"midjourney_8" => Self::Midjourney8,
"nano_banana" => Self::NanoBanana,
"nano_banana_2" => Self::NanoBanana2,
"nano_banana_pro" => Self::NanoBananaPro,
"seedream_4" => Self::Seedream4,
"seedream_4p5" => Self::Seedream4p5,
"seedream_5_lite" => Self::Seedream5Lite,
"qwen_edit_2511_angles" => Self::QwenEdit2511Angles,
"flux_2_lora_angles" => Self::Flux2LoraAngles,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientImageModel> for String {
fn from(value: ApiClientImageModel) -> Self {
match value {
ApiClientImageModel::Flux1Dev => "flux_1_dev".to_string(),
ApiClientImageModel::Flux1Schnell => "flux_1_schnell".to_string(),
ApiClientImageModel::FluxPro11 => "flux_pro_1p1".to_string(),
ApiClientImageModel::FluxPro11Ultra => "flux_pro_1p1_ultra".to_string(),
ApiClientImageModel::GptImage1 => "gpt_image_1".to_string(),
ApiClientImageModel::GptImage1p5 => "gpt_image_1p5".to_string(),
ApiClientImageModel::GptImage2 => "gpt_image_2".to_string(),
ApiClientImageModel::GrokImagineImage => "grok_imagine_image".to_string(),
ApiClientImageModel::GrokImagineImageQuality => "grok_imagine_image_q".to_string(),
ApiClientImageModel::Midjourney7 => "midjourney_7".to_string(),
ApiClientImageModel::Midjourney7Niji => "midjourney_7_niji".to_string(),
ApiClientImageModel::Midjourney8 => "midjourney_8".to_string(),
ApiClientImageModel::NanoBanana => "nano_banana".to_string(),
ApiClientImageModel::NanoBanana2 => "nano_banana_2".to_string(),
ApiClientImageModel::NanoBananaPro => "nano_banana_pro".to_string(),
ApiClientImageModel::Seedream4 => "seedream_4".to_string(),
ApiClientImageModel::Seedream4p5 => "seedream_4p5".to_string(),
ApiClientImageModel::Seedream5Lite => "seedream_5_lite".to_string(),
ApiClientImageModel::QwenEdit2511Angles => "qwen_edit_2511_angles".to_string(),
ApiClientImageModel::Flux2LoraAngles => "flux_2_lora_angles".to_string(),
ApiClientImageModel::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientImageModel = serde_json::from_str("\"flux_1_dev\"").unwrap();
assert_eq!(parsed, ApiClientImageModel::Flux1Dev);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"flux_1_dev\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientImageModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientImageModel::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientImageModel::Flux1Dev, "flux_1_dev"),
(ApiClientImageModel::Flux1Schnell, "flux_1_schnell"),
(ApiClientImageModel::FluxPro11, "flux_pro_1p1"),
(ApiClientImageModel::FluxPro11Ultra, "flux_pro_1p1_ultra"),
(ApiClientImageModel::GptImage1, "gpt_image_1"),
(ApiClientImageModel::GptImage1p5, "gpt_image_1p5"),
(ApiClientImageModel::GptImage2, "gpt_image_2"),
(ApiClientImageModel::GrokImagineImage, "grok_imagine_image"),
(ApiClientImageModel::GrokImagineImageQuality, "grok_imagine_image_q"),
(ApiClientImageModel::Midjourney7, "midjourney_7"),
(ApiClientImageModel::Midjourney7Niji, "midjourney_7_niji"),
(ApiClientImageModel::Midjourney8, "midjourney_8"),
(ApiClientImageModel::NanoBanana, "nano_banana"),
(ApiClientImageModel::NanoBanana2, "nano_banana_2"),
(ApiClientImageModel::NanoBananaPro, "nano_banana_pro"),
(ApiClientImageModel::Seedream4, "seedream_4"),
(ApiClientImageModel::Seedream4p5, "seedream_4p5"),
(ApiClientImageModel::Seedream5Lite, "seedream_5_lite"),
(ApiClientImageModel::QwenEdit2511Angles, "qwen_edit_2511_angles"),
(ApiClientImageModel::Flux2LoraAngles, "flux_2_lora_angles"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientImageModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,147 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `ModelCreator` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientModelCreator {
Alibaba,
ArtCraft,
BlackForestLabs,
Bytedance,
Fal,
Google,
Grok,
Hailuo,
Higgsfield,
Kling,
Krea,
Midjourney,
OpenAi,
OpenArt,
Recraft,
Replicate,
Runway,
Stability,
Tencent,
TensorArt,
Vidu,
WorldLabs,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientModelCreator {
fn from(value: String) -> Self {
match value.as_str() {
"alibaba" => Self::Alibaba,
"artcraft" => Self::ArtCraft,
"black_forest_labs" => Self::BlackForestLabs,
"bytedance" => Self::Bytedance,
"fal" => Self::Fal,
"google" => Self::Google,
"grok" => Self::Grok,
"hailuo" => Self::Hailuo,
"higgsfield" => Self::Higgsfield,
"kling" => Self::Kling,
"krea" => Self::Krea,
"midjourney" => Self::Midjourney,
"open_ai" => Self::OpenAi,
"open_art" => Self::OpenArt,
"recraft" => Self::Recraft,
"replicate" => Self::Replicate,
"runway" => Self::Runway,
"stability" => Self::Stability,
"tencent" => Self::Tencent,
"tensor_art" => Self::TensorArt,
"vidu" => Self::Vidu,
"world_labs" => Self::WorldLabs,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientModelCreator> for String {
fn from(value: ApiClientModelCreator) -> Self {
match value {
ApiClientModelCreator::Alibaba => "alibaba".to_string(),
ApiClientModelCreator::ArtCraft => "artcraft".to_string(),
ApiClientModelCreator::BlackForestLabs => "black_forest_labs".to_string(),
ApiClientModelCreator::Bytedance => "bytedance".to_string(),
ApiClientModelCreator::Fal => "fal".to_string(),
ApiClientModelCreator::Google => "google".to_string(),
ApiClientModelCreator::Grok => "grok".to_string(),
ApiClientModelCreator::Hailuo => "hailuo".to_string(),
ApiClientModelCreator::Higgsfield => "higgsfield".to_string(),
ApiClientModelCreator::Kling => "kling".to_string(),
ApiClientModelCreator::Krea => "krea".to_string(),
ApiClientModelCreator::Midjourney => "midjourney".to_string(),
ApiClientModelCreator::OpenAi => "open_ai".to_string(),
ApiClientModelCreator::OpenArt => "open_art".to_string(),
ApiClientModelCreator::Recraft => "recraft".to_string(),
ApiClientModelCreator::Replicate => "replicate".to_string(),
ApiClientModelCreator::Runway => "runway".to_string(),
ApiClientModelCreator::Stability => "stability".to_string(),
ApiClientModelCreator::Tencent => "tencent".to_string(),
ApiClientModelCreator::TensorArt => "tensor_art".to_string(),
ApiClientModelCreator::Vidu => "vidu".to_string(),
ApiClientModelCreator::WorldLabs => "world_labs".to_string(),
ApiClientModelCreator::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientModelCreator = serde_json::from_str("\"alibaba\"").unwrap();
assert_eq!(parsed, ApiClientModelCreator::Alibaba);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"alibaba\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientModelCreator = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientModelCreator::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientModelCreator::Alibaba, "alibaba"),
(ApiClientModelCreator::ArtCraft, "artcraft"),
(ApiClientModelCreator::BlackForestLabs, "black_forest_labs"),
(ApiClientModelCreator::Bytedance, "bytedance"),
(ApiClientModelCreator::Fal, "fal"),
(ApiClientModelCreator::Google, "google"),
(ApiClientModelCreator::Grok, "grok"),
(ApiClientModelCreator::Hailuo, "hailuo"),
(ApiClientModelCreator::Higgsfield, "higgsfield"),
(ApiClientModelCreator::Kling, "kling"),
(ApiClientModelCreator::Krea, "krea"),
(ApiClientModelCreator::Midjourney, "midjourney"),
(ApiClientModelCreator::OpenAi, "open_ai"),
(ApiClientModelCreator::OpenArt, "open_art"),
(ApiClientModelCreator::Recraft, "recraft"),
(ApiClientModelCreator::Replicate, "replicate"),
(ApiClientModelCreator::Runway, "runway"),
(ApiClientModelCreator::Stability, "stability"),
(ApiClientModelCreator::Tencent, "tencent"),
(ApiClientModelCreator::TensorArt, "tensor_art"),
(ApiClientModelCreator::Vidu, "vidu"),
(ApiClientModelCreator::WorldLabs, "world_labs"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientModelCreator = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,71 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonQuality` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientQuality {
High,
Medium,
Low,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientQuality {
fn from(value: String) -> Self {
match value.as_str() {
"high" => Self::High,
"medium" => Self::Medium,
"low" => Self::Low,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientQuality> for String {
fn from(value: ApiClientQuality) -> Self {
match value {
ApiClientQuality::High => "high".to_string(),
ApiClientQuality::Medium => "medium".to_string(),
ApiClientQuality::Low => "low".to_string(),
ApiClientQuality::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientQuality = serde_json::from_str("\"high\"").unwrap();
assert_eq!(parsed, ApiClientQuality::High);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"high\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientQuality = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientQuality::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientQuality::High, "high"),
(ApiClientQuality::Medium, "medium"),
(ApiClientQuality::Low, "low"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientQuality = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,91 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonResolution` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientResolution {
OneK,
TwoK,
ThreeK,
FourK,
HalfK,
FourEightyP,
SevenTwentyP,
TenEightyP,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientResolution {
fn from(value: String) -> Self {
match value.as_str() {
"one_k" => Self::OneK,
"two_k" => Self::TwoK,
"three_k" => Self::ThreeK,
"four_k" => Self::FourK,
"half_k" => Self::HalfK,
"four_eighty_p" => Self::FourEightyP,
"seven_twenty_p" => Self::SevenTwentyP,
"ten_eighty_p" => Self::TenEightyP,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientResolution> for String {
fn from(value: ApiClientResolution) -> Self {
match value {
ApiClientResolution::OneK => "one_k".to_string(),
ApiClientResolution::TwoK => "two_k".to_string(),
ApiClientResolution::ThreeK => "three_k".to_string(),
ApiClientResolution::FourK => "four_k".to_string(),
ApiClientResolution::HalfK => "half_k".to_string(),
ApiClientResolution::FourEightyP => "four_eighty_p".to_string(),
ApiClientResolution::SevenTwentyP => "seven_twenty_p".to_string(),
ApiClientResolution::TenEightyP => "ten_eighty_p".to_string(),
ApiClientResolution::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientResolution = serde_json::from_str("\"one_k\"").unwrap();
assert_eq!(parsed, ApiClientResolution::OneK);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"one_k\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientResolution = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientResolution::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientResolution::OneK, "one_k"),
(ApiClientResolution::TwoK, "two_k"),
(ApiClientResolution::ThreeK, "three_k"),
(ApiClientResolution::FourK, "four_k"),
(ApiClientResolution::HalfK, "half_k"),
(ApiClientResolution::FourEightyP, "four_eighty_p"),
(ApiClientResolution::SevenTwentyP, "seven_twenty_p"),
(ApiClientResolution::TenEightyP, "ten_eighty_p"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientResolution = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,191 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonVideoModel` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientVideoModel {
GrokVideo,
GrokImagineVideo,
GrokImagineVideo1p5,
Kling16Pro,
Kling21Pro,
Kling21Master,
Kling2p5TurboPro,
Kling2p6Pro,
Kling3p0Standard,
Kling3p0Pro,
HappyHorse1p0,
Seedance10Lite,
Seedance1p5Pro,
Seedance2p0,
Seedance2p0Fast,
Seedance2p0BytePlus,
Seedance2p0BytePlusFast,
Seedance2p0Ultra,
Seedance2p0UltraFast,
Seedance2p0BytePlusUltra,
Seedance2p0BytePlusUltraFast,
Seedance2p0Mini,
Seedance2p0BytePlusMini,
Seedance2p0BytePlusUltraMini,
Sora2,
Sora2Pro,
Veo2,
Veo3,
Veo3Fast,
Veo3p1,
Veo3p1Fast,
PreviewModel,
PreviewModelFast,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientVideoModel {
fn from(value: String) -> Self {
match value.as_str() {
"grok_video" => Self::GrokVideo,
"grok_imagine_video" => Self::GrokImagineVideo,
"grok_imagine_video_1p5" => Self::GrokImagineVideo1p5,
"kling_1p6_pro" => Self::Kling16Pro,
"kling_2p1_pro" => Self::Kling21Pro,
"kling_2p1_master" => Self::Kling21Master,
"kling_2p5_turbo_pro" => Self::Kling2p5TurboPro,
"kling_2p6_pro" => Self::Kling2p6Pro,
"kling_3p0_standard" => Self::Kling3p0Standard,
"kling_3p0_pro" => Self::Kling3p0Pro,
"happy_horse_1p0" => Self::HappyHorse1p0,
"seedance_1p0_lite" => Self::Seedance10Lite,
"seedance_1p5_pro" => Self::Seedance1p5Pro,
"seedance_2p0" => Self::Seedance2p0,
"seedance_2p0_fast" => Self::Seedance2p0Fast,
"seedance_2p0_bp" => Self::Seedance2p0BytePlus,
"seedance_2p0_bp_fast" => Self::Seedance2p0BytePlusFast,
"seedance_2p0_u" => Self::Seedance2p0Ultra,
"seedance_2p0_u_fast" => Self::Seedance2p0UltraFast,
"seedance_2p0_bpu" => Self::Seedance2p0BytePlusUltra,
"seedance_2p0_bpu_fast" => Self::Seedance2p0BytePlusUltraFast,
"seedance_2p0_mini" => Self::Seedance2p0Mini,
"seedance_2p0_bp_mini" => Self::Seedance2p0BytePlusMini,
"seedance_2p0_bpu_mini" => Self::Seedance2p0BytePlusUltraMini,
"sora_2" => Self::Sora2,
"sora_2_pro" => Self::Sora2Pro,
"veo_2" => Self::Veo2,
"veo_3" => Self::Veo3,
"veo_3_fast" => Self::Veo3Fast,
"veo_3p1" => Self::Veo3p1,
"veo_3p1_fast" => Self::Veo3p1Fast,
"preview_model" => Self::PreviewModel,
"preview_model_fast" => Self::PreviewModelFast,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientVideoModel> for String {
fn from(value: ApiClientVideoModel) -> Self {
match value {
ApiClientVideoModel::GrokVideo => "grok_video".to_string(),
ApiClientVideoModel::GrokImagineVideo => "grok_imagine_video".to_string(),
ApiClientVideoModel::GrokImagineVideo1p5 => "grok_imagine_video_1p5".to_string(),
ApiClientVideoModel::Kling16Pro => "kling_1p6_pro".to_string(),
ApiClientVideoModel::Kling21Pro => "kling_2p1_pro".to_string(),
ApiClientVideoModel::Kling21Master => "kling_2p1_master".to_string(),
ApiClientVideoModel::Kling2p5TurboPro => "kling_2p5_turbo_pro".to_string(),
ApiClientVideoModel::Kling2p6Pro => "kling_2p6_pro".to_string(),
ApiClientVideoModel::Kling3p0Standard => "kling_3p0_standard".to_string(),
ApiClientVideoModel::Kling3p0Pro => "kling_3p0_pro".to_string(),
ApiClientVideoModel::HappyHorse1p0 => "happy_horse_1p0".to_string(),
ApiClientVideoModel::Seedance10Lite => "seedance_1p0_lite".to_string(),
ApiClientVideoModel::Seedance1p5Pro => "seedance_1p5_pro".to_string(),
ApiClientVideoModel::Seedance2p0 => "seedance_2p0".to_string(),
ApiClientVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
ApiClientVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
ApiClientVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
ApiClientVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini".to_string(),
ApiClientVideoModel::Sora2 => "sora_2".to_string(),
ApiClientVideoModel::Sora2Pro => "sora_2_pro".to_string(),
ApiClientVideoModel::Veo2 => "veo_2".to_string(),
ApiClientVideoModel::Veo3 => "veo_3".to_string(),
ApiClientVideoModel::Veo3Fast => "veo_3_fast".to_string(),
ApiClientVideoModel::Veo3p1 => "veo_3p1".to_string(),
ApiClientVideoModel::Veo3p1Fast => "veo_3p1_fast".to_string(),
ApiClientVideoModel::PreviewModel => "preview_model".to_string(),
ApiClientVideoModel::PreviewModelFast => "preview_model_fast".to_string(),
ApiClientVideoModel::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientVideoModel = serde_json::from_str("\"grok_video\"").unwrap();
assert_eq!(parsed, ApiClientVideoModel::GrokVideo);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"grok_video\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientVideoModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientVideoModel::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientVideoModel::GrokVideo, "grok_video"),
(ApiClientVideoModel::GrokImagineVideo, "grok_imagine_video"),
(ApiClientVideoModel::GrokImagineVideo1p5, "grok_imagine_video_1p5"),
(ApiClientVideoModel::Kling16Pro, "kling_1p6_pro"),
(ApiClientVideoModel::Kling21Pro, "kling_2p1_pro"),
(ApiClientVideoModel::Kling21Master, "kling_2p1_master"),
(ApiClientVideoModel::Kling2p5TurboPro, "kling_2p5_turbo_pro"),
(ApiClientVideoModel::Kling2p6Pro, "kling_2p6_pro"),
(ApiClientVideoModel::Kling3p0Standard, "kling_3p0_standard"),
(ApiClientVideoModel::Kling3p0Pro, "kling_3p0_pro"),
(ApiClientVideoModel::HappyHorse1p0, "happy_horse_1p0"),
(ApiClientVideoModel::Seedance10Lite, "seedance_1p0_lite"),
(ApiClientVideoModel::Seedance1p5Pro, "seedance_1p5_pro"),
(ApiClientVideoModel::Seedance2p0, "seedance_2p0"),
(ApiClientVideoModel::Seedance2p0Fast, "seedance_2p0_fast"),
(ApiClientVideoModel::Seedance2p0BytePlus, "seedance_2p0_bp"),
(ApiClientVideoModel::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast"),
(ApiClientVideoModel::Seedance2p0Ultra, "seedance_2p0_u"),
(ApiClientVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast"),
(ApiClientVideoModel::Seedance2p0BytePlusUltra, "seedance_2p0_bpu"),
(ApiClientVideoModel::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast"),
(ApiClientVideoModel::Seedance2p0Mini, "seedance_2p0_mini"),
(ApiClientVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini"),
(ApiClientVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"),
(ApiClientVideoModel::Sora2, "sora_2"),
(ApiClientVideoModel::Sora2Pro, "sora_2_pro"),
(ApiClientVideoModel::Veo2, "veo_2"),
(ApiClientVideoModel::Veo3, "veo_3"),
(ApiClientVideoModel::Veo3Fast, "veo_3_fast"),
(ApiClientVideoModel::Veo3p1, "veo_3p1"),
(ApiClientVideoModel::Veo3p1Fast, "veo_3p1_fast"),
(ApiClientVideoModel::PreviewModel, "preview_model"),
(ApiClientVideoModel::PreviewModelFast, "preview_model_fast"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientVideoModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -1,8 +0,0 @@
pub mod api_client_aspect_ratio;
pub mod api_client_bitrate;
pub mod api_client_generation_provider;
pub mod api_client_image_model;
pub mod api_client_model_creator;
pub mod api_client_quality;
pub mod api_client_resolution;
pub mod api_client_video_model;
@@ -1 +0,0 @@
pub mod generation;
@@ -1 +0,0 @@
pub mod common_enums;
@@ -1,2 +1 @@
pub mod generate;
pub mod models;
@@ -1 +0,0 @@
pub mod omni_gen_list_image_models;
@@ -1,237 +0,0 @@
//! Self-contained, forward-compatible binding for `GET /v1/omni_gen/models/image`.
//!
//! IMPORTANT: This module deliberately does NOT reuse any types from
//! `artcraft_api_defs`. Client builds may be deployed in the wild long after the
//! server's models, enums, and response shapes have changed, so every request /
//! response type is copied here and made permissive:
//! - Unknown JSON fields are ignored (serde default behavior).
//! - Missing collection/flag fields default rather than fail (`serde(default)`).
//! - Every string-valued enum has an `Unknown(String)` catch-all so new server
//! variants deserialize instead of erroring.
use serde_derive::{Deserialize, Serialize};
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
use crate::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
use crate::datatypes::common_enums::generation::api_client_image_model::ApiClientImageModel;
use crate::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
use crate::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
use crate::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
pub const OMNI_GEN_IMAGE_MODELS_PATH: &str = "/v1/omni_gen/models/image";
/// Arguments for [`omni_gen_list_image_models`].
pub struct OmniGenListImageModelsArgs<'a> {
pub api_host: &'a ApiHost,
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
/// Which provider's models to list. `None` lets the server default (artcraft).
pub provider: Option<OmniGenImageModelsProvider>,
}
/// List available image models.
pub async fn omni_gen_list_image_models(
args: OmniGenListImageModelsArgs<'_>,
) -> Result<OmniGenImageModelsResponse, StorytellerError> {
let path = match args.provider {
Some(provider) => format!("{}?provider={}", OMNI_GEN_IMAGE_MODELS_PATH, provider.as_query_value()),
None => OMNI_GEN_IMAGE_MODELS_PATH.to_string(),
};
Ok(basic_json_get_request(args.api_host, &path, args.maybe_creds).await?)
}
/// The provider filter for the models endpoint (a client-supplied request value).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OmniGenImageModelsProvider {
/// Only models available through ArtCraft.
Artcraft,
/// All known models across all providers.
All,
}
impl OmniGenImageModelsProvider {
pub fn as_query_value(self) -> &'static str {
match self {
Self::Artcraft => "artcraft",
Self::All => "all",
}
}
}
impl Default for OmniGenImageModelsProvider {
fn default() -> Self {
Self::Artcraft
}
}
// ============================ Response types ============================
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelsResponse {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub models: Vec<OmniGenImageModelDetails>,
#[serde(default)]
pub providers: Vec<OmniGenImageModelProviderDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelProviderDetails {
pub provider: ApiClientGenerationProvider,
#[serde(default)]
pub models: Vec<OmniGenImageProviderModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageProviderModelDetails {
pub model: ApiClientImageModel,
#[serde(default)]
pub overrides: Option<OmniGenImageModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelDetails {
pub model: ApiClientImageModel,
#[serde(default)]
pub model_creator: Option<ApiClientModelCreator>,
#[serde(default)]
pub full_name: Option<String>,
#[serde(default)]
pub text_prompt_supported: Option<bool>,
#[serde(default)]
pub text_prompt_max_length: Option<u16>,
#[serde(default)]
pub negative_text_prompt_supported: Option<bool>,
#[serde(default)]
pub negative_text_prompt_max_length: Option<u16>,
#[serde(default)]
pub image_refs_supported: Option<bool>,
#[serde(default)]
pub image_refs_max: Option<u16>,
#[serde(default)]
pub has_fixed_editing_aspect_ratio: Option<bool>,
#[serde(default)]
pub aspect_ratio_options: Option<Vec<ApiClientAspectRatio>>,
#[serde(default)]
pub aspect_ratio_default: Option<ApiClientAspectRatio>,
#[serde(default)]
pub aspect_ratio_default_when_editing: Option<ApiClientAspectRatio>,
#[serde(default)]
pub resolution_options: Option<Vec<ApiClientResolution>>,
#[serde(default)]
pub resolution_default: Option<ApiClientResolution>,
#[serde(default)]
pub quality_options: Option<Vec<ApiClientQuality>>,
#[serde(default)]
pub default_quality: Option<ApiClientQuality>,
#[serde(default)]
pub batch_size_min: Option<u16>,
#[serde(default)]
pub batch_size_max: Option<u16>,
#[serde(default)]
pub batch_size_options: Option<Vec<u16>>,
#[serde(default)]
pub batch_size_default: Option<u16>,
#[serde(default)]
pub is_disabled: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn response_tolerates_unknown_fields_and_variants() {
let json = r#"{
"success": true,
"models": [
{
"model": "brand_new_image_model",
"model_creator": "some_new_studio",
"aspect_ratio_options": ["square", "some_new_ratio"],
"surprise_new_field": 123
}
],
"providers": [
{ "provider": "new_provider", "models": [ { "model": "nano_banana_pro" } ] }
],
"another_unexpected_top_level_field": "ignored"
}"#;
let resp: OmniGenImageModelsResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.models.len(), 1);
assert_eq!(resp.models[0].model, ApiClientImageModel::Unknown("brand_new_image_model".to_string()));
assert_eq!(resp.models[0].model_creator, Some(ApiClientModelCreator::Unknown("some_new_studio".to_string())));
let ratios = resp.models[0].aspect_ratio_options.as_ref().unwrap();
assert_eq!(ratios[0], ApiClientAspectRatio::Square);
assert_eq!(ratios[1], ApiClientAspectRatio::Unknown("some_new_ratio".to_string()));
assert_eq!(resp.models[0].is_disabled, None);
assert_eq!(resp.providers[0].provider, ApiClientGenerationProvider::Unknown("new_provider".to_string()));
assert_eq!(resp.providers[0].models[0].model, ApiClientImageModel::NanoBananaPro);
}
#[test]
fn empty_object_uses_defaults() {
let resp: OmniGenImageModelsResponse = serde_json::from_str("{}").unwrap();
assert!(!resp.success);
assert!(resp.models.is_empty());
assert!(resp.providers.is_empty());
}
}
/// Live tests that hit real servers. All `#[ignore]` so they never run by default.
/// Run explicitly, e.g.:
/// cargo test -p artcraft_client omni_gen_list_image_models::live_tests -- --ignored --nocapture
#[cfg(test)]
mod live_tests {
mod localhost {
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits http://localhost:12345
async fn list_image_models() {
let api_host = ApiHost::Localhost { port: 12345 };
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[localhost] image models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one image model");
}
}
mod production {
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits https://api.storyteller.ai
async fn list_image_models() {
let api_host = ApiHost::Storyteller;
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[production] image models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one image model");
}
}
}
@@ -1,2 +0,0 @@
pub mod image;
pub mod video;
@@ -1 +0,0 @@
pub mod omni_gen_list_video_models;
@@ -1,291 +0,0 @@
//! Self-contained, forward-compatible binding for `GET /v1/omni_gen/models/video`.
//!
//! IMPORTANT: This module deliberately does NOT reuse any types from
//! `artcraft_api_defs`. Client builds may be deployed in the wild long after the
//! server's models, enums, and response shapes have changed, so every request /
//! response type is copied here and made permissive:
//! - Unknown JSON fields are ignored (serde default behavior).
//! - Missing collection/flag fields default rather than fail (`serde(default)`).
//! - Every string-valued enum has an `Unknown(String)` catch-all so new server
//! variants deserialize instead of erroring.
use serde_derive::{Deserialize, Serialize};
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use crate::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
use crate::datatypes::common_enums::generation::api_client_bitrate::ApiClientBitrate;
use crate::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
use crate::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
use crate::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
use crate::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
use crate::datatypes::common_enums::generation::api_client_video_model::ApiClientVideoModel;
pub const OMNI_GEN_VIDEO_MODELS_PATH: &str = "/v1/omni_gen/models/video";
/// Arguments for [`omni_gen_list_video_models`].
pub struct OmniGenListVideoModelsArgs<'a> {
pub api_host: &'a ApiHost,
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
/// Which provider's models to list. `None` lets the server default (artcraft).
pub provider: Option<OmniGenVideoModelsProvider>,
}
/// List available video models.
pub async fn omni_gen_list_video_models(
args: OmniGenListVideoModelsArgs<'_>,
) -> Result<OmniGenVideoModelsResponse, StorytellerError> {
let path = match args.provider {
Some(provider) => format!("{}?provider={}", OMNI_GEN_VIDEO_MODELS_PATH, provider.as_query_value()),
None => OMNI_GEN_VIDEO_MODELS_PATH.to_string(),
};
Ok(basic_json_get_request(args.api_host, &path, args.maybe_creds).await?)
}
/// The provider filter for the models endpoint (a client-supplied request value).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OmniGenVideoModelsProvider {
/// Only models available through ArtCraft.
Artcraft,
/// All known models across all providers.
All,
}
impl OmniGenVideoModelsProvider {
pub fn as_query_value(self) -> &'static str {
match self {
Self::Artcraft => "artcraft",
Self::All => "all",
}
}
}
impl Default for OmniGenVideoModelsProvider {
fn default() -> Self {
Self::Artcraft
}
}
// ============================ Response types ============================
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenVideoModelsResponse {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub models: Vec<OmniGenVideoModelDetails>,
#[serde(default)]
pub providers: Vec<OmniGenVideoModelProviderDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenVideoModelProviderDetails {
pub provider: ApiClientGenerationProvider,
#[serde(default)]
pub models: Vec<OmniGenVideoProviderModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenVideoProviderModelDetails {
pub model: ApiClientVideoModel,
#[serde(default)]
pub overrides: Option<OmniGenVideoModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenVideoModelDetails {
pub model: ApiClientVideoModel,
#[serde(default)]
pub model_creator: Option<ApiClientModelCreator>,
#[serde(default)]
pub full_name: Option<String>,
#[serde(default)]
pub extra_info: Option<String>,
#[serde(default)]
pub extra_info_short: Option<String>,
#[serde(default)]
pub text_to_video_supported: Option<bool>,
#[serde(default)]
pub text_prompt_supported: Option<bool>,
#[serde(default)]
pub text_prompt_max_length: Option<u16>,
#[serde(default)]
pub negative_text_prompt_supported: Option<bool>,
#[serde(default)]
pub negative_text_prompt_max_length: Option<u16>,
#[serde(default)]
pub starting_keyframe_supported: Option<bool>,
#[serde(default)]
pub starting_keyframe_required: Option<bool>,
#[serde(default)]
pub ending_keyframe_supported: Option<bool>,
#[serde(default)]
pub image_references_supported: Option<bool>,
#[serde(default)]
pub image_references_max: Option<u16>,
#[serde(default)]
pub video_references_supported: Option<bool>,
#[serde(default)]
pub video_references_max: Option<u16>,
#[serde(default)]
pub video_references_max_total_duration_seconds: Option<u16>,
#[serde(default)]
pub audio_references_supported: Option<bool>,
#[serde(default)]
pub audio_references_max: Option<u16>,
#[serde(default)]
pub audio_references_max_total_duration_seconds: Option<u16>,
#[serde(default)]
pub character_references_supported: Option<bool>,
#[serde(default)]
pub character_references_max: Option<u16>,
#[serde(default)]
pub show_generate_with_sound_toggle: Option<bool>,
#[serde(default)]
pub aspect_ratio_options: Option<Vec<ApiClientAspectRatio>>,
#[serde(default)]
pub aspect_ratio_default: Option<ApiClientAspectRatio>,
#[serde(default)]
pub resolution_options: Option<Vec<ApiClientResolution>>,
#[serde(default)]
pub resolution_default: Option<ApiClientResolution>,
#[serde(default)]
pub bitrate_options: Option<Vec<ApiClientBitrate>>,
#[serde(default)]
pub bitrate_default: Option<ApiClientBitrate>,
#[serde(default)]
pub quality_options: Option<Vec<ApiClientQuality>>,
#[serde(default)]
pub default_quality: Option<ApiClientQuality>,
#[serde(default)]
pub duration_seconds_min: Option<u16>,
#[serde(default)]
pub duration_seconds_max: Option<u16>,
#[serde(default)]
pub duration_seconds_max_with_image_references: Option<u16>,
#[serde(default)]
pub duration_seconds_options: Option<Vec<u16>>,
#[serde(default)]
pub duration_seconds_default: Option<u16>,
#[serde(default)]
pub batch_size_min: Option<u16>,
#[serde(default)]
pub batch_size_max: Option<u16>,
#[serde(default)]
pub batch_size_options: Option<Vec<u16>>,
#[serde(default)]
pub batch_size_default: Option<u16>,
#[serde(default)]
pub is_disabled: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_enum_value_is_preserved_and_round_trips() {
let known: ApiClientVideoModel = serde_json::from_str("\"seedance_2p0\"").unwrap();
assert_eq!(known, ApiClientVideoModel::Seedance2p0);
assert_eq!(serde_json::to_string(&known).unwrap(), "\"seedance_2p0\"");
// A value this build has never heard of does NOT fail — it is captured verbatim.
let unknown: ApiClientVideoModel = serde_json::from_str("\"future_model_9000\"").unwrap();
assert_eq!(unknown, ApiClientVideoModel::Unknown("future_model_9000".to_string()));
// ...and serializes back to exactly what came in.
assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"future_model_9000\"");
}
#[test]
fn response_tolerates_unknown_fields_and_variants() {
let json = r#"{
"success": true,
"models": [
{
"model": "brand_new_model",
"model_creator": "some_new_studio",
"aspect_ratio_options": ["wide_sixteen_by_nine", "some_new_ratio"],
"surprise_new_field": 123
}
],
"providers": [
{ "provider": "new_provider", "models": [ { "model": "seedance_2p0" } ] }
],
"another_unexpected_top_level_field": "ignored"
}"#;
let resp: OmniGenVideoModelsResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.models.len(), 1);
assert_eq!(resp.models[0].model, ApiClientVideoModel::Unknown("brand_new_model".to_string()));
assert_eq!(resp.models[0].model_creator, Some(ApiClientModelCreator::Unknown("some_new_studio".to_string())));
let ratios = resp.models[0].aspect_ratio_options.as_ref().unwrap();
assert_eq!(ratios[0], ApiClientAspectRatio::WideSixteenByNine);
assert_eq!(ratios[1], ApiClientAspectRatio::Unknown("some_new_ratio".to_string()));
assert_eq!(resp.models[0].is_disabled, None); // missing optional -> None
assert_eq!(resp.providers[0].provider, ApiClientGenerationProvider::Unknown("new_provider".to_string()));
assert_eq!(resp.providers[0].models[0].model, ApiClientVideoModel::Seedance2p0);
}
#[test]
fn empty_object_uses_defaults() {
let resp: OmniGenVideoModelsResponse = serde_json::from_str("{}").unwrap();
assert!(!resp.success);
assert!(resp.models.is_empty());
assert!(resp.providers.is_empty());
}
}
/// Live tests that hit real servers. All `#[ignore]` so they never run by default.
/// Run explicitly, e.g.:
/// cargo test -p artcraft_client omni_gen_list_video_models::live_tests -- --ignored --nocapture
#[cfg(test)]
mod live_tests {
mod localhost {
use super::super::{omni_gen_list_video_models, OmniGenListVideoModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits http://localhost:12345
async fn list_video_models() {
let api_host = ApiHost::Localhost { port: 12345 };
let response = omni_gen_list_video_models(OmniGenListVideoModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[localhost] video models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one video model");
}
}
mod production {
use super::super::{omni_gen_list_video_models, OmniGenListVideoModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits https://api.storyteller.ai
async fn list_video_models() {
let api_host = ApiHost::Storyteller;
let response = omni_gen_list_video_models(OmniGenListVideoModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[production] video models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one video model");
}
}
}
@@ -1,7 +1,6 @@
//! An API client for Artcraft / Storyteller / FakeYou
pub mod credentials;
pub mod datatypes;
pub mod endpoints;
pub mod error;
pub mod recipes;
@@ -65,15 +65,6 @@ pub enum RouterVideoModel {
#[serde(rename = "seedance_2p0_bpu_fast")]
Seedance2p0BytePlusUltraFast,
#[serde(rename = "seedance_2p0_mini")]
Seedance2p0Mini,
#[serde(rename = "seedance_2p0_bp_mini")]
Seedance2p0BytePlusMini,
#[serde(rename = "seedance_2p0_bpu_mini")]
Seedance2p0BytePlusUltraMini,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
@@ -104,23 +95,3 @@ pub enum RouterVideoModel {
#[serde(rename = "preview_model_fast")]
PreviewModelFast,
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_serde_round_trip(model: RouterVideoModel, expected: &str) {
let json = serde_json::to_string(&model).unwrap();
assert_eq!(json, format!("\"{expected}\""));
let parsed: RouterVideoModel = serde_json::from_str(&json).unwrap();
// RouterVideoModel isn't PartialEq, so round-trip back to the wire form.
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
}
#[test]
fn seedance_2p0_mini_variants_serialize() {
assert_serde_round_trip(RouterVideoModel::Seedance2p0Mini, "seedance_2p0_mini");
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
}
}
@@ -138,7 +138,6 @@ pub(crate) fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviMidjourneyBatchCount::One),
2 => Ok(KinoviMidjourneyBatchCount::Two),
3 => Ok(KinoviMidjourneyBatchCount::Three),
4 => Ok(KinoviMidjourneyBatchCount::Four),
other => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
@@ -147,11 +146,13 @@ pub(crate) fn plan_batch_count(
value: format!("{}", other),
}))
}
// Kinovi supports batches of 1–4; only counts of 5+ reach here, so both
// mitigation strategies clamp to the maximum supported batch.
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(KinoviMidjourneyBatchCount::Four)
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
// 3 → 4, anything > 4 → 4
Ok(if other < 3 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
// 3 → 2, anything > 4 → 4
Ok(if other < 4 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
},
}
@@ -378,10 +379,9 @@ mod tests {
}
#[test]
fn one_two_three_four_map_directly() {
fn one_two_four_map_directly() {
assert_eq!(batch_for(Some(1)), KinoviMidjourneyBatchCount::One);
assert_eq!(batch_for(Some(2)), KinoviMidjourneyBatchCount::Two);
assert_eq!(batch_for(Some(3)), KinoviMidjourneyBatchCount::Three);
assert_eq!(batch_for(Some(4)), KinoviMidjourneyBatchCount::Four);
}
@@ -394,57 +394,43 @@ mod tests {
));
}
/// Kinovi now supports batches of 3, so 3 is a first-class value and the
/// mitigation strategy no longer applies to it.
#[test]
fn three_maps_directly_regardless_of_strategy() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
request_mismatch_mitigation_strategy: strategy,
..base_builder()
};
assert_eq!(
unwrap_direct_request(build_kinovi_midjourney_7(builder)).request.batch_count,
KinoviMidjourneyBatchCount::Three,
"strategy={:?}", strategy,
);
}
}
/// Counts above 4 are still unsupported: strict errors out, and both
/// pay-more/pay-less strategies clamp to the maximum supported batch (4).
#[test]
fn five_errors_under_strict_and_clamps_to_four_otherwise() {
let strict = GenerateImageRequestBuilder {
image_batch_count: Some(5),
fn three_errors_under_strict() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_builder()
};
assert!(matches!(
build_kinovi_midjourney_7(strict),
build_kinovi_midjourney_7(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })),
));
}
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_builder()
};
assert_eq!(
unwrap_direct_request(build_kinovi_midjourney_7(builder)).request.batch_count,
KinoviMidjourneyBatchCount::Four,
"strategy={:?}", strategy,
);
}
#[test]
fn three_upgrades_to_four_with_pay_more() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
assert_eq!(
unwrap_direct_request(build_kinovi_midjourney_7(builder)).request.batch_count,
KinoviMidjourneyBatchCount::Four,
);
}
#[test]
fn three_downgrades_to_two_with_pay_less() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..base_builder()
};
assert_eq!(
unwrap_direct_request(build_kinovi_midjourney_7(builder)).request.batch_count,
KinoviMidjourneyBatchCount::Two,
);
}
}
@@ -126,7 +126,6 @@ pub(crate) fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviMidjourneyBatchCount::One),
2 => Ok(KinoviMidjourneyBatchCount::Two),
3 => Ok(KinoviMidjourneyBatchCount::Three),
4 => Ok(KinoviMidjourneyBatchCount::Four),
other => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
@@ -135,11 +134,11 @@ pub(crate) fn plan_batch_count(
value: format!("{}", other),
}))
}
// Kinovi supports batches of 1–4; only counts of 5+ reach here, so both
// mitigation strategies clamp to the maximum supported batch.
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(KinoviMidjourneyBatchCount::Four)
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if other < 3 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if other < 4 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
},
}
@@ -213,16 +212,6 @@ mod tests {
));
}
#[test]
fn batch_count_three_is_accepted() {
let builder = GenerateImageRequestBuilder { image_batch_count: Some(3), ..base_builder() };
let req = match build_kinovi_midjourney_7_niji(builder).expect("build") {
ImageGenerationDraftOrRequest::Request(ImageGenerationRequest::KinoviMidjourney7Niji(r)) => r,
_ => panic!("expected Request"),
};
assert_eq!(req.request.batch_count, KinoviMidjourneyBatchCount::Three);
}
#[test]
fn batch_count_four_is_accepted() {
let builder = GenerateImageRequestBuilder { image_batch_count: Some(4), ..base_builder() };
@@ -126,7 +126,6 @@ pub(crate) fn plan_batch_count(
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviMidjourneyBatchCount::One),
2 => Ok(KinoviMidjourneyBatchCount::Two),
3 => Ok(KinoviMidjourneyBatchCount::Three),
4 => Ok(KinoviMidjourneyBatchCount::Four),
other => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
@@ -135,11 +134,11 @@ pub(crate) fn plan_batch_count(
value: format!("{}", other),
}))
}
// Kinovi supports batches of 1–4; only counts of 5+ reach here, so both
// mitigation strategies clamp to the maximum supported batch.
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(KinoviMidjourneyBatchCount::Four)
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if other < 3 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if other < 4 { KinoviMidjourneyBatchCount::Two } else { KinoviMidjourneyBatchCount::Four })
}
},
}
@@ -202,16 +201,6 @@ mod tests {
));
}
#[test]
fn batch_count_three_is_accepted() {
let builder = GenerateImageRequestBuilder { image_batch_count: Some(3), ..base_builder() };
let req = match build_kinovi_midjourney_8(builder).expect("build") {
ImageGenerationDraftOrRequest::Request(ImageGenerationRequest::KinoviMidjourney8(r)) => r,
_ => panic!("expected Request"),
};
assert_eq!(req.request.batch_count, KinoviMidjourneyBatchCount::Three);
}
#[test]
fn batch_count_four_is_accepted() {
let builder = GenerateImageRequestBuilder { image_batch_count: Some(4), ..base_builder() };
@@ -32,9 +32,6 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::build
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_fast::build::build_artcraft_seedance_2p0_bp_fast;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::build::build_artcraft_seedance_2p0_bpu;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_fast::build::build_artcraft_seedance_2p0_bpu_fast;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::build::build_artcraft_seedance_2p0_mini;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::build::build_artcraft_seedance_2p0_bp_mini;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::build::build_artcraft_seedance_2p0_bpu_mini;
use crate::generate::generate_video::providers::artcraft::sora_2::build::build_artcraft_sora_2;
use crate::generate::generate_video::providers::artcraft::sora_2_pro::build::build_artcraft_sora_2_pro;
use crate::generate::generate_video::providers::artcraft::veo_2::build::build_artcraft_veo_2;
@@ -65,7 +62,6 @@ use crate::generate::generate_video::providers::fal::veo_3_fast::build::build_fa
use crate::generate::generate_video::providers::fal::veo_3p1::build::build_fal_veo_3p1;
use crate::generate::generate_video::providers::fal::veo_3p1_fast::build::build_fal_veo_3p1_fast;
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
/// RouterProvider-agnostic video generation request. Distilled by `build2()` into a
@@ -180,9 +176,6 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltra) => build_artcraft_seedance_2p0_bpu(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraFast) => build_artcraft_seedance_2p0_bpu_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Mini) => build_artcraft_seedance_2p0_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusMini) => build_artcraft_seedance_2p0_bp_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self),
(RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
@@ -221,9 +214,6 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Seedance2Pro, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -21,8 +21,6 @@ use crate::generate::generate_video::providers::artcraft::resolve::{
pub enum SupportedResolutions {
/// 480p, 720p, 1080p
Full,
/// 480p, 720p, 1080p, 4K
FullWith4k,
/// 480p, 720p only (1080p downgrades to 720p)
Fast,
/// 720p, 1080p only (480p upgrades)
@@ -159,7 +157,7 @@ fn plan_output_resolution(
None => Ok(None),
Some(RouterResolution::FourEightyP) => match supported {
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::Fast => {
SupportedResolutions::Full | SupportedResolutions::Fast => {
Ok(Some(CommonResolutionEnum::FourEightyP))
}
SupportedResolutions::NoFourEightyP => match strategy {
@@ -177,7 +175,7 @@ fn plan_output_resolution(
Some(RouterResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
Some(RouterResolution::TenEightyP) => match supported {
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::NoFourEightyP => {
SupportedResolutions::Full | SupportedResolutions::NoFourEightyP => {
Ok(Some(CommonResolutionEnum::TenEightyP))
}
SupportedResolutions::Fast => match strategy {
@@ -191,20 +189,6 @@ fn plan_output_resolution(
},
},
// 4K is only available on models flagged `FullWith4k` (the non-Fast Seedance
// 2.0 family). Other models error out or fall back to their best resolution.
Some(RouterResolution::FourK) => match (supported, strategy) {
(SupportedResolutions::FullWith4k, _) => Ok(Some(CommonResolutionEnum::FourK)),
(_, RequestMismatchMitigationStrategy::ErrorOut) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "resolution",
value: format!("{:?}", RouterResolution::FourK),
}))
}
(SupportedResolutions::Fast, _) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
(_, _) => Ok(Some(CommonResolutionEnum::TenEightyP)),
},
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {

Some files were not shown because too many files have changed in this diff Show More