Compare commits

...
Author SHA1 Message Date
Brandon Thomas 67dd65c636 beeble endpoint 2026-06-09 20:12:17 -04:00
Brandon Thomas cc4203ff3e const def 2026-06-09 20:08:16 -04:00
Brandon Thomas 6c053aba7c add a few more clients for testing 2026-06-09 20:04:53 -04:00
Brandon Thomas 671fe5892b add generation platform type (web, desktop, etc.) 2026-06-09 19:10:10 -04:00
Brandon Thomas 70fb0b784f folders - better media files links, last items, etc. in the API (#1618) 2026-06-09 09:48:19 -04:00
Brandon Thomas e9f46b1e87 folders fixes, new endpoint for atomic "move" (#1617) 2026-06-08 19:09:23 -04:00
BFlatandTanish Manku 9caaad5e0d Page video editor: Mount and make public (#1611)
Co-authored-by: Tanish Manku <tanish2k09.dev@gmail.com>
2026-06-08 13:01:51 -07:00
Brandon Thomas 8253e28560 folders (#1616) 2026-06-08 12:09:02 -04:00
bflatastic 7af6e5b867 Webapp: added missing seedance variant name mappings 2026-06-08 22:19:32 +07:00
bflatastic ccdcb01035 fix(webapp): clamp duration to nearest valid value on model switch instead of resetting to min 2026-06-08 21:18:23 +07:00
BFlat e3a8c04878 Webapp: OmniGen outage handling + toast fixes (#1615)
### Outage handling
- Show an error toast when the OmniGen service is unavailable (5xx)
instead of failing silently.
- Distinct messages for the two failure points: couldn't load the model
list vs. couldn't enqueue a generation.
- Model-fetch toast fires once per failed attempt (singleton fetch +
message dedup); generation toast is gated to 5xx so content-policy/4xx
rejections still just show the failed card and 402 still opens the
credits modal.
2026-06-08 20:59:50 +07:00
BFlat 3886253ffa Webapp: prompt-box fixes, in-progress recreate, model-name consistency, sticky settings (#1614)
### Prompt box
- Fix prompt box overflowing past the viewport at some desktop widths
(Generate button hidden) - removed `w-full` so it sizes to the content
area beside the sidebar (create-image + create-video).

### Recreate action
- Add **Recreate** to in-progress jobs in the create-page gallery (grid
+ list) and the task queue - previously only on completed/failed.
- Task queue had no Recreate at all; added it to all three states
(in-progress/completed/failed) for consistency.
- Recreate on the in-progress grid card sits bottom-right inline with
the prompt; in-progress list row reveals it on hover (matching completed
rows).
- Extract shared `useRecreateFromPromptToken` hook; refactor
FailedCard/FailedRow onto it (DRY).

### Model display names
- Consolidate to a single source of truth: webapp `getModelDisplayName`
now delegates to `@storyteller/model-list` (removed the duplicate local
map). Gallery, task queue, and lightbox now agree.
- Fix missing names: `gpt_image_1` (gallery) and Midjourney in the
lightbox - added `midjourney_7` / `midjourney_7_niji` / `midjourney_8`
to the model-list name + creator-icon maps; aligned `veo_2` → "Google
Veo 2".

### Sticky prompt settings across model switches
- Prompt-box settings no longer reset when switching models - the user's
choice persists and is resolved against the new model (kept if
supported, else falls back for display/generation only, without losing
the preference).
- Covers aspect ratio, count, resolution, quality (image) and aspect
ratio, resolution, count, duration, sound (video).
- Add `lib/resolve-model-setting.ts` (`resolveModelOption` /
`resolveModelCount`); `handleModelChange` now only switches the model (+
capability-bound input mode on video).
2026-06-08 19:35:20 +07:00
bflatastic c1b257b7d5 updated download links to v0.32.0 2026-06-06 07:56:31 +07:00
bflatastic b162e2bfc0 webapp: fixes to mobile sheet component on mobile 2026-06-06 07:50:07 +07:00
BFlat 3ea33dabd5 test fix 2: mobile freezing after image modal closed on mobile (#1613) 2026-06-06 07:37:15 +07:00
BFlat aee20db947 test fix: mobile freezing after image modal closed on mobile (#1612) 2026-06-06 07:20:20 +07:00
BFlat 3847a3d39c Revert "Page video editor: Mount and make public" (#1610)
Reverts storytold/artcraft#1606
2026-06-06 06:59:01 +07:00
BFlat 5747212f37 Revert "fix(ci): use pnpm instead of npm in artcraft publish workflows" (#1609)
Reverts storytold/artcraft#1608
2026-06-06 06:58:27 +07:00
Tanish Mankuandbflatastic 04ebe2c965 fix(ci): use pnpm instead of npm in artcraft publish workflows (#1608)
The frontend migrated to pnpm workspaces in #1606, which introduced
"workspace:*" dependencies. npm cannot resolve that protocol, so the
macOS and Windows publish workflows fail at "npm ci" with
EUNSUPPORTEDPROTOCOL, breaking production release builds on main.

Add a pinned pnpm setup step and replace the npm install block with
"pnpm install --frozen-lockfile". Verified the frozen install resolves
cleanly against the committed pnpm-lock.yaml.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-06 06:49:16 +07:00
Tanish Mankuandbflatastic d09d44b7e2 Page video editor: Mount and make public (#1606)
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-05 22:21:55 +07:00
BFlat 8e754b4b8f website: landing buttons and footer cta updates (#1607)
- Renamed the "Supercharge Credits" button to **Use on Web** (globe
icon) in the hero and bottom CTA. Routes subscribers to the webapp
homepage, everyone else to `/pricing`.
- Removed the "Create image / Create video" links from both sections.
- Split Discord + GitHub into a separate "Open source &
community-driven" CTA (subtle container) above the "Ready to craft?"
CTA.
2026-06-05 14:47:30 +07:00
BFlat cbda6b7b1f fix(webapp): prevent iOS caret loss in mobile prompt input (#1605)
MentionTextarea's DOM-sync effect rewrote innerHTML under the live caret
whenever buildHTML's identity changed, detaching the cursor on iOS
Safari mid-typing. Skip the rewrite when focused and the DOM already
matches value, and memoize colorMap in MobilePromptForm to stop the
per-render churn.
2026-06-04 13:05:30 +07:00
Brandon Thomas 186ce24390 fix routing 2026-06-03 23:59:13 -04:00
Brandon Thomas 3401e709bb rename handlers + codegen fix 2026-06-03 10:57:27 -04:00
Brandon Thomas f71d344ff3 admin email changes (#1604) 2026-06-03 10:37:28 -04:00
772 changed files with 86642 additions and 4334 deletions
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.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": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"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": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "05a880a2ece0236f57fe1c40100cba0c0cdcad2ebee0fecb88f9043ac7b82d8c"
}
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nLIMIT 1\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": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"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": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "0c918aacf4ce34e1e03d2f9af49dd3138cadf59a28d0917726135ed5f34cba5e"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 30
"Right": 31
},
"nullable": []
},
"hash": "161febbb30cf6c669a813fe036d405ef428422297440abd240482a6aae51f6f9"
"hash": "108731dc51d915ebb8b6ed391c5bae4c41ae6f2c813be92c07d8a10ca401db53"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_email_changes\nSET\n user_token = ?,\n old_email = ?,\n new_email = ?,\n ip_address = ?,\n maybe_changed_by_user_token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "1359fb54af12d78251fe435b063771984ee00d74a71b5d29e9df267991f38522"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_cover_image_custom_media_token = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "197e4fb3822b095cbbf8127493dad91c9254b7c242b8921a8c2df9f5a0e77424"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `request_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "1ddb2207db50e2dbfb37d26c006bffe2d8f5fb011855d5a86d01f395c36df759"
"hash": "1a0b60602debe525529f50566fc545f1cb4b77005f423a10464652be7fbfa839"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET has_star = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "24716cfdb77eee365bd605a40cecb02b3a27a90725dbe96e3865545d14839ab8"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n\n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -127,9 +137,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "31d85cc0013f29e6db2d272ee6e709cca79b46496a2c222960f1a1419175f00b"
"hash": "38a1d7b676bb0987fc191bbc8bdeadf6174a19358dff40d9772b9d5824145bbc"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n \n jobs.maybe_external_third_party as `external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n jobs.maybe_external_third_party_id as `external_third_party_id`,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n \n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,\n\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n\n jobs.created_at,\n jobs.updated_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\nAND jobs.maybe_external_third_party_id = ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -127,9 +137,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "2c98621c08055374fd09502c94ac40bb01d23fcea3f844cceea7446a5c0cb4dc"
"hash": "3a846909a725eb776b4a4637dc64dcaf1ed70ad8891f9d012c7f3cdcb16a613e"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_creator_user_token as `maybe_creator_user_token: UserToken`,\n media_class as `media_class: MediaFileClass`,\n media_type as `media_type: MediaFileType`,\n maybe_cover_image_media_file_token as `maybe_cover_image_media_file_token: MediaFileToken`\nFROM media_files\nWHERE token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_creator_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "media_class: MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 2,
"name": "media_type: MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "maybe_cover_image_media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true
]
},
"hash": "5606a694c872d868ab3b35f4b3685f86f2b27ad16e741c8b1bf997b613f1b803"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\n AND uec.id < ?\nORDER BY uec.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": "old_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 2,
"name": "new_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 4,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 5,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "user_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "user_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "user_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_changed_by_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_changed_by_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 12,
"name": "maybe_changed_by_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,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true
]
},
"hash": "5611678b6c168208ea4da9ef0e3071758c9bb8d89b54e2d3f0fc84bd0a6c915f"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
"hash": "5f24aeb12e5a78ffedd1c772c594b668188aaaf01f315a1c41c75610fdfc1b32"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_color_code = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "6a0e3f012795068a56a0dfaf567d066dc157bb06c736c1c4314b18c8e0356127"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nWITH RECURSIVE ancestor_chain (token, parent_token, depth) AS (\n SELECT token, maybe_parent_folder_token, 0\n FROM folders\n WHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\n UNION ALL\n SELECT f.token, f.maybe_parent_folder_token, ac.depth + 1\n FROM folders f\n INNER JOIN ancestor_chain ac\n ON f.token = ac.parent_token\n WHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND ac.depth < ?\n)\nSELECT token as `token!: FolderToken`\nFROM ancestor_chain\nWHERE depth > 0\nORDER BY depth ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token!: FolderToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
true
]
},
"hash": "780891e8294b78cc0e5a05a8b5a8949a763bcb8c5d6526a621234020b154bdc9"
}
@@ -0,0 +1,267 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "added_to_folder_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 2,
"name": "media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "media_class: MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "media_type: MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_batch_token: BatchGenerationToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 17,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 18,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 19,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 20,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 21,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 22,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "88673127b54626dc38d28693e709dfaeced9a05300a26b3f3c52a17f28171c9a"
}
@@ -0,0 +1,168 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.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": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"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": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "a13680fb2b54809b6d9d9356f8eec87cd0f1f37405b84bbf734ed06cc244efb1"
}
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_generation_provider = ?,\n\n platform_type = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 41
"Right": 42
},
"nullable": []
},
"hash": "a4901f904aa1c00d9afc23faa7008816c6e1b653bd6ad9f0b6772e32000be544"
"hash": "ad2ffbd9f48e52678a5c61817bec043bf1df4033463e5c5a18468c04af391e6a"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET\n maybe_last_media_file_token_1 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 0\n ),\n maybe_last_media_file_token_2 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 1\n ),\n maybe_last_media_file_token_3 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 2\n ),\n maybe_last_media_file_token_4 = (\n SELECT mf.token\n FROM folder_media_files fmf\n JOIN media_files mf\n ON mf.token = fmf.media_file_token\n WHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n ORDER BY fmf.id DESC\n LIMIT 1 OFFSET 3\n )\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "ae417955113b385031181772b29f234ab9297cf583583f74fe287bf9c24b1e6a"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO folders\nSET\n token = ?,\n name = ?,\n owner_user_token = ?,\n maybe_parent_folder_token = ?,\n maybe_color_code = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "c099b274026619760f0982f0e5082da1e8de16c26122f2750667b9cdc2c4abf3"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\nORDER BY uec.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": "old_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 2,
"name": "new_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 4,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 5,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "user_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "user_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "user_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_changed_by_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_changed_by_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 12,
"name": "maybe_changed_by_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,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true
]
},
"hash": "c17e1d6b567dd378bb4cb0478fb9558e907a0f36498bd049c7c526e496e89259"
}
@@ -0,0 +1,267 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND fmf.id < ?\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "added_to_folder_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 2,
"name": "media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "media_class: MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "media_type: MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_batch_token: BatchGenerationToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | ENUM",
"char_set": 224,
"max_size": 28
}
},
{
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 14,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | MULTIPLE_KEY",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 15,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 16,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 17,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 18,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 19,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 20,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 21,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 22,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "d2a406bcf08f656fd602aa6ba1124a77d04ad874b15abafca300e8b381c7b211"
}
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.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": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"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": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "d6f0c65abce2da20fe86805ff053b8c2569bf0095f6245a5529f0c71854a604b"
}
@@ -0,0 +1,168 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.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": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"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": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "d85c1a844f14b9416c6260d7a906fec709b50eb4583b98dffb52ebbe6abcb7d5"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -82,6 +82,16 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_platform_type: enums::common::platform_type::PlatformType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
}
],
"parameters": {
@@ -95,8 +105,9 @@
false,
false,
true,
true,
true
]
},
"hash": "a0002183d4d077b07bb0cd062ca95bf113d1564ce3195f0e62e531ce4297dd76"
"hash": "ddbbd3886c00268edd3993508f635549fc8ddc877bb059fd58f14b8c07586fc9"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET name = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "e32e195ef41bc833e5b7fc53cfde8669f88aeb66d279e5213d047f4860ec86ab"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_deleted_at = NOW()\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "f9e3ff8ca682948076b799d6b2d934be4a87a4e04665d6f12cc32095494129ad"
}
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_email_changes;
@@ -0,0 +1,35 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS folders;
@@ -0,0 +1,56 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folders (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC, used by the API).
token VARCHAR(32) NOT NULL,
-- User-supplied folder name.
name VARCHAR(255) NOT NULL,
-- The user that owns this folder.
owner_user_token VARCHAR(32) NOT NULL,
-- Parent folder for nesting, NULL for top-level folders.
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
-- The last four media files added to the folder, in order. These are
-- denormalized for cheap thumbnail-grid rendering on folder cards
-- without a separate fan-out query.
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen cover image (a media file). Overrides the
-- auto-grid thumbnail derived from the last-N media files above.
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
-- short named-color fallback.
maybe_color_code VARCHAR(16) DEFAULT NULL,
-- Whether the user has starred / favorited this folder.
has_star BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the folder is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_owner_user_token (owner_user_token),
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
KEY index_folders_created_at (created_at),
KEY index_folders_updated_at (updated_at),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS folder_media_files;
@@ -0,0 +1,27 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folder_media_files (
-- Auto-increment for posterity / row-creation ordering. Not the primary
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
-- requires its own key, hence the UNIQUE KEY below.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The folder this membership belongs to.
folder_token VARCHAR(32) NOT NULL,
-- The media file in the folder.
media_file_token VARCHAR(32) NOT NULL,
-- When the media file was added to the folder. Rows are hard-deleted
-- when a media file is removed, so there is no `maybe_deleted_at`.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (folder_token, media_file_token),
UNIQUE KEY (id),
KEY index_folder_token (folder_token),
KEY index_media_file_token (media_file_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN platform_type;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
DROP COLUMN platform_type;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE media_files
ADD COLUMN platform_type VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,32 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- N:M mapping between `folders` and `media_files`. Rows are hard-deleted
-- when a media file is removed from a folder — no soft-delete column.
CREATE TABLE folder_media_files (
-- Auto-increment for posterity / row-creation ordering. Not the primary
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
-- requires its own key, hence the UNIQUE KEY below.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The folder this membership belongs to.
folder_token VARCHAR(32) NOT NULL,
-- The media file in the folder.
media_file_token VARCHAR(32) NOT NULL,
-- When the media file was added to the folder. Rows are hard-deleted
-- when a media file is removed, so there is no `maybe_deleted_at`.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (folder_token, media_file_token),
UNIQUE KEY (id),
KEY index_folder_token (folder_token),
KEY index_media_file_token (media_file_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,59 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folders (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC, used by the API).
token VARCHAR(32) NOT NULL,
-- User-supplied folder name.
name VARCHAR(255) NOT NULL,
-- The user that owns this folder.
owner_user_token VARCHAR(32) NOT NULL,
-- Parent folder for nesting, NULL for top-level folders.
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
-- The last four media files added to the folder, in order. These are
-- denormalized for cheap thumbnail-grid rendering on folder cards
-- without a separate fan-out query.
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen cover image (a media file). Overrides the
-- auto-grid thumbnail derived from the last-N media files above.
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
-- short named-color fallback.
maybe_color_code VARCHAR(16) DEFAULT NULL,
-- Whether the user has starred / favorited this folder.
has_star BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the folder is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_owner_user_token (owner_user_token),
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
KEY index_folders_created_at (created_at),
KEY index_folders_updated_at (updated_at),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -208,6 +208,11 @@ CREATE TABLE generic_inference_jobs (
-- Multiple debug_logs rows can share the same event token.
maybe_debug_log_event_token VARCHAR(32) DEFAULT NULL,
-- The platform the enqueuing request came from ('web', 'desktop_app',
-- 'curl', 'python_requests', 'postman'),
-- inferred from the request's User-Agent header.
platform_type VARCHAR(16) DEFAULT NULL,
-- ========== JOB SYSTEM DETAILS ==========
-- Jobs begin as "pending", then transition to other states.
@@ -65,6 +65,12 @@ CREATE TABLE media_files (
-- What service provided the generation (if any).
maybe_generation_provider VARCHAR(16) DEFAULT NULL,
-- The platform the creating request came from ('web', 'desktop_app',
-- 'curl', 'python_requests', 'postman'),
-- inferred from the request's User-Agent header. Copied from the originating
-- generic_inference_jobs row for inference results.
platform_type VARCHAR(16) DEFAULT NULL,
-- For inference that can be tied back to a model, the type of model.
-- There are multiple indices on this column!
-- DO NOT EXPOSE THIS TO USERS VIA THE API, as we may leak secrets in doing so.
@@ -0,0 +1,38 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,69 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use url::Url;
use utoipa::ToSchema;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
// NB: The "folder media file" list-item shape lives in storyteller_web
// (see `endpoints/folders/media_files/list_folder_media_files_handler.rs`)
// because it embeds `MediaFileCoverImageDetails` and other domain types
// that depend on the request's `MediaDomain` + `ServerEnvironment`.
/// Canonical wire shape for a folder. Used by single-folder GETs, create
/// responses, list rows, and subfolder list rows.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct FolderInfo {
pub token: FolderToken,
pub name: String,
pub owner_user_token: UserToken,
pub maybe_parent_folder_token: Option<FolderToken>,
/// The four most-recent media files added to the folder, in slot order
/// (the schema stores them as `maybe_last_media_file_token_1..4`).
/// Slots whose media file has been deleted are skipped, so the vec
/// may contain 0-4 entries.
pub last_media_thumbnails: Vec<FolderThumbnail>,
/// User-chosen cover image. When present, the frontend should use this
/// as the folder's primary thumbnail instead of the
/// `last_media_thumbnails` grid.
pub maybe_custom_cover_thumbnail: Option<FolderThumbnail>,
pub maybe_color_code: Option<String>,
pub has_star: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
/// True when `maybe_parent_folder_token` is set but the referenced
/// parent row is missing or soft-deleted.
pub is_orphaned: bool,
}
/// A compact thumbnail descriptor for a media file referenced by a
/// folder (either one of the `last_media_*` slots or the custom cover).
///
/// Intentionally narrower than `MediaLinks` / `CoverImageLinks` — folders
/// only need the bare minimum to render a thumbnail card. The CDN URL is
/// the direct asset link; `maybe_thumbnail_template` carries the resizing
/// template URL (with `{{width}}` / `{{height}}` placeholders) when the
/// media class supports it.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct FolderThumbnail {
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
/// Direct CDN URL to the underlying media asset.
pub cdn_url: Url,
/// Resizing template URL (with `{{width}}` / `{{height}}` placeholders)
/// when the media class supports it. `None` for media classes without
/// a thumbnail variant.
pub maybe_thumbnail_template: Option<String>,
}
@@ -0,0 +1,116 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::folders::common::FolderInfo;
// ── POST /v1/folders/create ──
#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
pub name: String,
pub maybe_parent_folder_token: Option<FolderToken>,
pub maybe_color_code: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct CreateFolderSuccessResponse {
pub success: bool,
pub folder: FolderInfo,
}
// ── GET /v1/folders/list_all ──
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListFoldersQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListFoldersSuccessResponse {
pub success: bool,
pub folders: Vec<FolderInfo>,
pub maybe_cursor: Option<String>,
}
// ── GET /v1/folders/folder/{folder_token} ──
#[derive(Deserialize, ToSchema)]
pub struct FolderPathInfo {
pub folder_token: FolderToken,
}
#[derive(Serialize, ToSchema)]
pub struct GetFolderSuccessResponse {
pub success: bool,
pub folder: FolderInfo,
}
// ── PUT /v1/folders/folder/{folder_token}/rename ──
#[derive(Deserialize, ToSchema)]
pub struct RenameFolderRequest {
pub new_name: String,
}
#[derive(Serialize, ToSchema)]
pub struct RenameFolderSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/star ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderStarRequest {
pub has_star: bool,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderStarSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/color_code ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderColorCodeRequest {
/// `None` clears the color tag.
pub maybe_color_code: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderColorCodeSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/cover_image ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderCoverImageRequest {
/// `None` clears the cover image. If set, the server resolves it to a
/// usable cover-image media file token: image/video media files are
/// used directly; other types fall back to the file's own
/// `maybe_cover_image_media_file_token` (and fail if neither path
/// applies).
pub maybe_media_file_token: Option<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderCoverImageSuccessResponse {
pub success: bool,
/// The media file token that ended up being stored as the cover image
/// (may differ from the requested token if the server resolved through
/// the file's own cover image). `None` when the cover was cleared.
pub maybe_resolved_cover_media_file_token: Option<MediaFileToken>,
}
// ── DELETE /v1/folders/folder/{folder_token} ──
#[derive(Serialize, ToSchema)]
pub struct DeleteFolderSuccessResponse {
pub success: bool,
}
@@ -0,0 +1,88 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
// ── GET /v1/folders/media_files/{folder_token} ──
//
// NB: `ListFolderMediaFilesSuccessResponse` and `FolderMediaFileListItem`
// live in `storyteller_web`'s handler because the wire shape embeds
// `MediaFileCoverImageDetails` and `MediaLinks` constructors that depend
// on the request's `MediaDomain` + `ServerEnvironment`.
#[derive(Deserialize, ToSchema)]
pub struct FolderMediaFilesPathInfo {
pub folder_token: FolderToken,
}
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListFolderMediaFilesQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
#[derive(Deserialize, ToSchema)]
pub struct BulkAddFolderMediaFilesRequest {
/// Media files to add to the folder. Tokens that don't exist or are
/// soft-deleted are silently skipped. Already-added rows are no-ops
/// (idempotent via INSERT IGNORE on the composite primary key).
pub media_file_tokens: Vec<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkAddFolderMediaFilesSuccessResponse {
pub success: bool,
/// Tokens that pre-existed (existed + not deleted). Already-added rows
/// are included since the operation is idempotent.
pub accepted_media_file_tokens: Vec<MediaFileToken>,
}
// ── PUT /v1/folders/media_files/{folder_token}/bulk_remove ──
#[derive(Deserialize, ToSchema)]
pub struct BulkRemoveFolderMediaFilesRequest {
pub media_file_tokens: Vec<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkRemoveFolderMediaFilesSuccessResponse {
pub success: bool,
/// How many membership rows were actually hard-deleted.
pub removed_count: u64,
}
// ── POST /v1/folders/media_files/{folder_token}/bulk_move ──
/// Atomically move the listed media files from `source_folder` to the
/// URL `folder_token` (destination). Idempotent on both sides: tokens
/// that aren't currently in the source are silently skipped, and tokens
/// that are already in the destination stay there.
#[derive(Deserialize, ToSchema)]
pub struct BulkMoveFolderMediaFilesRequest {
pub source_folder: FolderToken,
pub media_file_tokens: Vec<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkMoveFolderMediaFilesSuccessResponse {
pub success: bool,
/// Number of membership rows actually deleted from the source folder.
pub removed_from_source_count: u64,
/// Number of membership rows actually inserted into the destination
/// folder. May be lower than the input length because (a) tokens that
/// were already present in the destination don't insert, and (b)
/// tokens whose media file doesn't exist / is soft-deleted are filtered
/// out before the insert.
pub added_to_destination_count: u64,
/// The media file tokens that ended up resident in the destination
/// folder after the move — i.e. the subset of the input that actually
/// referred to live media files. Includes both newly-inserted rows and
/// rows that were already there.
pub accepted_media_file_tokens: Vec<MediaFileToken>,
}
@@ -0,0 +1,4 @@
pub mod common;
pub mod folder;
pub mod media_files;
pub mod subfolder;
@@ -0,0 +1,61 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use crate::folders::common::FolderInfo;
// ── GET /v1/folders/subfolders/{folder_token} ──
#[derive(Deserialize, ToSchema)]
pub struct SubfolderPathInfo {
pub folder_token: FolderToken,
}
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListSubfoldersQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListSubfoldersSuccessResponse {
pub success: bool,
pub subfolders: Vec<FolderInfo>,
pub maybe_cursor: Option<String>,
}
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_add ──
#[derive(Deserialize, ToSchema)]
pub struct BulkAddSubfoldersRequest {
/// The folders to reparent under the URL `folder_token`. Tokens that
/// don't exist, aren't owned by the caller, are soft-deleted, or are
/// the parent itself are silently skipped (idempotent).
pub subfolder_tokens: Vec<FolderToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkAddSubfoldersSuccessResponse {
pub success: bool,
/// Tokens that were actually accepted (existed + owned + not deleted +
/// not the parent itself). Already-parented tokens are included here
/// since the operation is idempotent.
pub accepted_subfolder_tokens: Vec<FolderToken>,
}
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_remove ──
#[derive(Deserialize, ToSchema)]
pub struct BulkRemoveSubfoldersRequest {
/// Folders to unparent. Only rows currently parented to the URL
/// `folder_token` are affected; others are silently skipped.
pub subfolder_tokens: Vec<FolderToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkRemoveSubfoldersSuccessResponse {
pub success: bool,
/// How many rows were actually unparented.
pub removed_count: u64,
}
@@ -2,6 +2,7 @@ pub mod analytics;
pub mod characters;
pub mod common;
pub mod credits;
pub mod folders;
pub mod generate;
pub mod jobs;
pub mod media_file;
@@ -0,0 +1,27 @@
use chrono::{DateTime, Utc};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
/// Materialized representation of a `folders` row plus the derived
/// `is_orphaned` flag (true when `maybe_parent_folder_token` is set but the
/// referenced parent row is missing or soft-deleted).
#[derive(Debug, Clone)]
pub struct FolderRow {
pub id: u64,
pub token: FolderToken,
pub name: String,
pub owner_user_token: UserToken,
pub maybe_parent_folder_token: Option<FolderToken>,
pub maybe_last_media_file_token_1: Option<MediaFileToken>,
pub maybe_last_media_file_token_2: Option<MediaFileToken>,
pub maybe_last_media_file_token_3: Option<MediaFileToken>,
pub maybe_last_media_file_token_4: Option<MediaFileToken>,
pub maybe_cover_image_custom_media_token: Option<MediaFileToken>,
pub maybe_color_code: Option<String>,
pub has_star: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_orphaned: bool,
}
@@ -0,0 +1,82 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::folders::folder::folder_row::FolderRow;
pub struct GetFolderForOwnerArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Fetch a single (live) folder by token, scoped to an owner. Returns
/// `Ok(None)` if the folder doesn't exist, is soft-deleted, or is owned
/// by a different user (don't leak existence of others' folders).
pub async fn get_folder_for_owner<'e, 'c: 'e, E>(
args: GetFolderForOwnerArgs<'e, 'c, E>,
) -> Result<Option<FolderRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
SELECT
f.id as `id: u64`,
f.token as `token: FolderToken`,
f.name,
f.owner_user_token as `owner_user_token: UserToken`,
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
f.maybe_color_code,
f.has_star as `has_star: bool`,
f.created_at,
f.updated_at,
(
f.maybe_parent_folder_token IS NOT NULL
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
) as `is_orphaned: bool`
FROM folders f
LEFT JOIN folders p
ON p.token = f.maybe_parent_folder_token
WHERE f.token = ?
AND f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
LIMIT 1
"#,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.fetch_optional(args.mysql_executor)
.await?;
Ok(result.map(|r| FolderRow {
id: r.id,
token: r.token,
name: r.name,
owner_user_token: r.owner_user_token,
maybe_parent_folder_token: r.maybe_parent_folder_token,
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
maybe_color_code: r.maybe_color_code,
has_star: r.has_star,
created_at: r.created_at,
updated_at: r.updated_at,
is_orphaned: r.is_orphaned,
}))
}
@@ -0,0 +1,50 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct InsertFolderArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub name: &'e str,
pub owner_user_token: &'e UserToken,
pub maybe_parent_folder_token: Option<&'e FolderToken>,
pub maybe_color_code: Option<&'e str>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Insert a new folder row. The token is generated by this function and
/// returned to the caller (mints in the data-access layer, not the handler).
pub async fn insert_folder<'e, 'c: 'e, E>(
args: InsertFolderArgs<'e, 'c, E>,
) -> Result<FolderToken, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let token = FolderToken::generate();
sqlx::query!(
r#"
INSERT INTO folders
SET
token = ?,
name = ?,
owner_user_token = ?,
maybe_parent_folder_token = ?,
maybe_color_code = ?
"#,
token.as_str(),
args.name,
args.owner_user_token.as_str(),
args.maybe_parent_folder_token.map(|t| t.as_str()),
args.maybe_color_code,
)
.execute(args.mysql_executor)
.await?;
Ok(token)
}
@@ -0,0 +1,70 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
/// Maximum hops the recursive CTE will walk before giving up. Prevents
/// runaway recursion on already-corrupt data (a folder graph that
/// somehow contains a cycle).
const MAX_ANCESTOR_DEPTH: i64 = 64;
pub struct ListAncestorFolderTokensArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
/// The folder whose ancestor chain we're walking. Its own token is
/// NOT included in the result — only strict ancestors.
pub folder_token: &'e FolderToken,
/// Scope to a single owner so we never traverse into another user's
/// folder tree, even if the parent pointer were ever cross-owner.
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Return the strict ancestors of the given folder, walking up the
/// `maybe_parent_folder_token` chain. Order is nearest-ancestor first.
/// Deleted folders are excluded — a soft-deleted parent terminates the
/// chain. The walk is capped at [`MAX_ANCESTOR_DEPTH`] hops.
pub async fn list_ancestor_folder_tokens<'e, 'c: 'e, E>(
args: ListAncestorFolderTokensArgs<'e, 'c, E>,
) -> Result<Vec<FolderToken>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let rows = sqlx::query!(
r#"
WITH RECURSIVE ancestor_chain (token, parent_token, depth) AS (
SELECT token, maybe_parent_folder_token, 0
FROM folders
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
UNION ALL
SELECT f.token, f.maybe_parent_folder_token, ac.depth + 1
FROM folders f
INNER JOIN ancestor_chain ac
ON f.token = ac.parent_token
WHERE f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
AND ac.depth < ?
)
SELECT token as `token!: FolderToken`
FROM ancestor_chain
WHERE depth > 0
ORDER BY depth ASC
"#,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
args.owner_user_token.as_str(),
MAX_ANCESTOR_DEPTH,
)
.fetch_all(args.mysql_executor)
.await?;
Ok(rows.into_iter().map(|r| r.token).collect())
}
@@ -0,0 +1,154 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::folders::folder::folder_row::FolderRow;
pub struct ListFoldersForUserArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub owner_user_token: &'e UserToken,
pub maybe_cursor_id: Option<u64>,
pub limit: u32,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// List all live folders owned by a user, newest first. Includes the
/// derived `is_orphaned` flag computed by joining the table against itself
/// on `maybe_parent_folder_token` — orphaned means the parent row is
/// missing or soft-deleted.
pub async fn list_folders_for_user<'e, 'c: 'e, E>(
args: ListFoldersForUserArgs<'e, 'c, E>,
) -> Result<Vec<FolderRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let limit = args.limit as i64;
// Two query branches because sqlx::query! can't accept a conditional
// WHERE clause at the macro level. `args.mysql_executor` is moved into
// exactly one arm at runtime — Rust permits this since the arms are
// mutually exclusive.
let rows = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query!(
r#"
SELECT
f.id as `id: u64`,
f.token as `token: FolderToken`,
f.name,
f.owner_user_token as `owner_user_token: UserToken`,
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
f.maybe_color_code,
f.has_star as `has_star: bool`,
f.created_at,
f.updated_at,
(
f.maybe_parent_folder_token IS NOT NULL
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
) as `is_orphaned: bool`
FROM folders f
LEFT JOIN folders p
ON p.token = f.maybe_parent_folder_token
WHERE f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
AND f.id < ?
ORDER BY f.id DESC
LIMIT ?
"#,
args.owner_user_token.as_str(),
cursor_id,
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderRow {
id: r.id,
token: r.token,
name: r.name,
owner_user_token: r.owner_user_token,
maybe_parent_folder_token: r.maybe_parent_folder_token,
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
maybe_color_code: r.maybe_color_code,
has_star: r.has_star,
created_at: r.created_at,
updated_at: r.updated_at,
is_orphaned: r.is_orphaned,
})
.collect::<Vec<_>>()
}
None => {
sqlx::query!(
r#"
SELECT
f.id as `id: u64`,
f.token as `token: FolderToken`,
f.name,
f.owner_user_token as `owner_user_token: UserToken`,
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
f.maybe_color_code,
f.has_star as `has_star: bool`,
f.created_at,
f.updated_at,
(
f.maybe_parent_folder_token IS NOT NULL
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
) as `is_orphaned: bool`
FROM folders f
LEFT JOIN folders p
ON p.token = f.maybe_parent_folder_token
WHERE f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
ORDER BY f.id DESC
LIMIT ?
"#,
args.owner_user_token.as_str(),
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderRow {
id: r.id,
token: r.token,
name: r.name,
owner_user_token: r.owner_user_token,
maybe_parent_folder_token: r.maybe_parent_folder_token,
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
maybe_color_code: r.maybe_color_code,
has_star: r.has_star,
created_at: r.created_at,
updated_at: r.updated_at,
is_orphaned: r.is_orphaned,
})
.collect::<Vec<_>>()
}
};
Ok(rows)
}
@@ -0,0 +1,10 @@
pub mod folder_row;
pub mod get_folder_for_owner;
pub mod insert_folder;
pub mod list_ancestor_folder_tokens;
pub mod list_folders_for_user;
pub mod soft_delete_folder;
pub mod update_folder_color_code;
pub mod update_folder_cover_image;
pub mod update_folder_has_star;
pub mod update_folder_name;
@@ -0,0 +1,42 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct SoftDeleteFolderArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Mark a folder as deleted by stamping `maybe_deleted_at`. Children
/// retain their `maybe_parent_folder_token` and become "orphaned" — that
/// state is surfaced by the list query's `is_orphaned` flag.
pub async fn soft_delete_folder<'e, 'c: 'e, E>(
args: SoftDeleteFolderArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
UPDATE folders
SET maybe_deleted_at = NOW()
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
LIMIT 1
"#,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,42 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct UpdateFolderColorCodeArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub maybe_color_code: Option<&'e str>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Set or clear (`None`) the color code on a folder.
pub async fn update_folder_color_code<'e, 'c: 'e, E>(
args: UpdateFolderColorCodeArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
UPDATE folders
SET maybe_color_code = ?
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
LIMIT 1
"#,
args.maybe_color_code,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,46 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
pub struct UpdateFolderCoverImageArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
/// `None` clears the cover image.
pub maybe_cover_image_media_file_token: Option<&'e MediaFileToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Set or clear (`None`) the custom cover-image media file on a folder.
pub async fn update_folder_cover_image<'e, 'c: 'e, E>(
args: UpdateFolderCoverImageArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
UPDATE folders
SET maybe_cover_image_custom_media_token = ?
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
LIMIT 1
"#,
args.maybe_cover_image_media_file_token.map(|t| t.as_str()),
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,41 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct UpdateFolderHasStarArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub has_star: bool,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub async fn update_folder_has_star<'e, 'c: 'e, E>(
args: UpdateFolderHasStarArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
UPDATE folders
SET has_star = ?
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
LIMIT 1
"#,
args.has_star,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,44 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct UpdateFolderNameArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub new_name: &'e str,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Rename a folder. Scoped to owner so callers can't rename a folder they
/// don't own. Returns the number of rows updated (0 if no live folder
/// matched).
pub async fn update_folder_name<'e, 'c: 'e, E>(
args: UpdateFolderNameArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
UPDATE folders
SET name = ?
WHERE token = ?
AND owner_user_token = ?
AND maybe_deleted_at IS NULL
LIMIT 1
"#,
args.new_name,
args.folder_token.as_str(),
args.owner_user_token.as_str(),
)
.execute(args.mysql_executor)
.await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,45 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
pub struct BulkDeleteFolderMediaFilesArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub media_file_tokens: &'e [MediaFileToken],
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Hard-delete membership rows for the given folder + media file tokens.
/// Idempotent — rows that don't exist are silently skipped. Returns the
/// number of rows actually deleted.
pub async fn bulk_delete_folder_media_files<'e, 'c: 'e, E>(
args: BulkDeleteFolderMediaFilesArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.media_file_tokens.is_empty() {
return Ok(0);
}
let mut builder = QueryBuilder::<MySql>::new(
"DELETE FROM folder_media_files WHERE folder_token = ",
);
builder.push_bind(args.folder_token.as_str());
builder.push(" AND media_file_token IN (");
let mut separated = builder.separated(", ");
for token in args.media_file_tokens {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let result = builder.build().execute(args.mysql_executor).await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,43 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
pub struct BulkInsertFolderMediaFilesArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub media_file_tokens: &'e [MediaFileToken],
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Insert membership rows for every (folder_token, media_file_token) in
/// the input. Uses `INSERT IGNORE` so already-present rows don't cause
/// duplicate-key errors — idempotent. Returns the number of rows
/// actually inserted.
pub async fn bulk_insert_folder_media_files<'e, 'c: 'e, E>(
args: BulkInsertFolderMediaFilesArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.media_file_tokens.is_empty() {
return Ok(0);
}
let folder_token_str = args.folder_token.as_str();
let mut builder = QueryBuilder::<MySql>::new(
"INSERT IGNORE INTO folder_media_files (folder_token, media_file_token) ",
);
builder.push_values(args.media_file_tokens, |mut b, media_file_token| {
b.push_bind(folder_token_str)
.push_bind(media_file_token.as_str());
});
let result = builder.build().execute(args.mysql_executor).await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,44 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder, Row};
use tokens::tokens::media_files::MediaFileToken;
pub struct FilterExistingMediaFileTokensArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub candidate_tokens: &'e [MediaFileToken],
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Return only the input tokens that currently exist and aren't
/// soft-deleted (no user_deleted_at or mod_deleted_at).
pub async fn filter_existing_media_file_tokens<'e, 'c: 'e, E>(
args: FilterExistingMediaFileTokensArgs<'e, 'c, E>,
) -> Result<Vec<MediaFileToken>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.candidate_tokens.is_empty() {
return Ok(Vec::new());
}
let mut builder = QueryBuilder::<MySql>::new(
"SELECT token FROM media_files WHERE user_deleted_at IS NULL \
AND mod_deleted_at IS NULL AND token IN (",
);
let mut separated = builder.separated(", ");
for token in args.candidate_tokens {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let rows = builder.build().fetch_all(args.mysql_executor).await?;
Ok(rows.into_iter()
.map(|row| MediaFileToken::new(row.get::<String, _>(0)))
.collect())
}
@@ -0,0 +1,249 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
/// One folder-media-file row + the joined media-file fields the list
/// endpoint needs to materialize MediaLinks, MediaFileCoverImageDetails,
/// and the rest of the typed response shape.
#[derive(Debug, Clone)]
pub struct FolderMediaFileRow {
/// `folder_media_files.id` — used as the pagination cursor.
pub membership_id: u64,
/// When the media file was added to the folder.
pub added_to_folder_at: DateTime<Utc>,
pub media_file_token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub maybe_batch_token: Option<BatchGenerationToken>,
// The media file's own bucket fields (caller uses these to build MediaLinks).
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
// Cover image's bucket fields, from a LEFT JOIN on
// mf.maybe_cover_image_media_file_token. All three are Some-or-None
// together (matched / unmatched join row).
pub maybe_cover_public_bucket_directory_hash: Option<String>,
pub maybe_cover_public_bucket_prefix: Option<String>,
pub maybe_cover_public_bucket_extension: Option<String>,
pub creator_set_visibility: Visibility,
pub is_user_upload: bool,
pub is_intermediate_system_file: bool,
pub maybe_title: Option<String>,
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_origin_filename: Option<String>,
pub maybe_duration_millis: Option<i32>,
pub maybe_frame_width: Option<i32>,
pub maybe_frame_height: Option<i32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub struct ListFolderMediaFilesArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub maybe_cursor_id: Option<u64>,
pub limit: u32,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Paginated list of media files in the given folder. Joins `media_files`
/// for the file's own metadata and LEFT JOINs `media_files` again on
/// `maybe_cover_image_media_file_token` so the cover-image bucket fields
/// come back in a single round-trip. Filters out media files that are
/// soft-deleted on the media-file side. Most-recently-added first.
///
/// The caller is expected to have already authorized the folder access.
pub async fn list_folder_media_files<'e, 'c: 'e, E>(
args: ListFolderMediaFilesArgs<'e, 'c, E>,
) -> Result<Vec<FolderMediaFileRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let limit = args.limit as i64;
let rows = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query!(
r#"
SELECT
fmf.id as `membership_id: u64`,
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
mf.token as `media_file_token: MediaFileToken`,
mf.media_class as `media_class: MediaFileClass`,
mf.media_type as `media_type: MediaFileType`,
mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,
mf.creator_set_visibility as `creator_set_visibility: Visibility`,
mf.is_user_upload as `is_user_upload: bool`,
mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,
mf.maybe_title,
mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,
mf.maybe_origin_filename,
mf.maybe_duration_millis,
mf.maybe_frame_width,
mf.maybe_frame_height,
mf.created_at,
mf.updated_at
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
LEFT JOIN media_files cover
ON cover.token = mf.maybe_cover_image_media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
AND fmf.id < ?
ORDER BY fmf.id DESC
LIMIT ?
"#,
args.folder_token.as_str(),
cursor_id,
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderMediaFileRow {
membership_id: r.membership_id,
added_to_folder_at: r.added_to_folder_at,
media_file_token: r.media_file_token,
media_class: r.media_class,
media_type: r.media_type,
maybe_batch_token: r.maybe_batch_token,
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
creator_set_visibility: r.creator_set_visibility,
is_user_upload: r.is_user_upload,
is_intermediate_system_file: r.is_intermediate_system_file,
maybe_title: r.maybe_title,
maybe_prompt_token: r.maybe_prompt_token,
maybe_origin_filename: r.maybe_origin_filename,
maybe_duration_millis: r.maybe_duration_millis,
maybe_frame_width: r.maybe_frame_width,
maybe_frame_height: r.maybe_frame_height,
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect::<Vec<_>>()
}
None => {
sqlx::query!(
r#"
SELECT
fmf.id as `membership_id: u64`,
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
mf.token as `media_file_token: MediaFileToken`,
mf.media_class as `media_class: MediaFileClass`,
mf.media_type as `media_type: MediaFileType`,
mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,
mf.creator_set_visibility as `creator_set_visibility: Visibility`,
mf.is_user_upload as `is_user_upload: bool`,
mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,
mf.maybe_title,
mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,
mf.maybe_origin_filename,
mf.maybe_duration_millis,
mf.maybe_frame_width,
mf.maybe_frame_height,
mf.created_at,
mf.updated_at
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
LEFT JOIN media_files cover
ON cover.token = mf.maybe_cover_image_media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
ORDER BY fmf.id DESC
LIMIT ?
"#,
args.folder_token.as_str(),
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderMediaFileRow {
membership_id: r.membership_id,
added_to_folder_at: r.added_to_folder_at,
media_file_token: r.media_file_token,
media_class: r.media_class,
media_type: r.media_type,
maybe_batch_token: r.maybe_batch_token,
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
creator_set_visibility: r.creator_set_visibility,
is_user_upload: r.is_user_upload,
is_intermediate_system_file: r.is_intermediate_system_file,
maybe_title: r.maybe_title,
maybe_prompt_token: r.maybe_prompt_token,
maybe_origin_filename: r.maybe_origin_filename,
maybe_duration_millis: r.maybe_duration_millis,
maybe_frame_width: r.maybe_frame_width,
maybe_frame_height: r.maybe_frame_height,
created_at: r.created_at,
updated_at: r.updated_at,
})
.collect::<Vec<_>>()
}
};
Ok(rows)
}
@@ -0,0 +1,5 @@
pub mod bulk_delete_folder_media_files;
pub mod bulk_insert_folder_media_files;
pub mod filter_existing_media_file_tokens;
pub mod list_folder_media_files;
pub mod recompute_folder_last_media_files;
@@ -0,0 +1,99 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
pub struct RecomputeFolderLastMediaFilesArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub folder_token: &'e FolderToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Recompute the four denormalized `maybe_last_media_file_token_1..4`
/// columns on `folders` for the given folder by scanning the current
/// (non-soft-deleted) members of `folder_media_files` and grabbing the
/// four with the largest `folder_media_files.id` (most-recently-added).
///
/// Single-statement: four correlated `LIMIT 1 OFFSET k` scalar subqueries
/// in the SET clause. Each subquery walks the `index_folder_token` index
/// on `folder_media_files`, joins `media_files` by primary key, and
/// stops at the (offset+1)-th matching row. Folders with fewer than four
/// live members get NULL automatically for the empty slots — that's the
/// scalar-subquery / no-rows-found semantics.
///
/// Intended to run inside the same transaction as the bulk add / remove /
/// move that necessitated the recompute, so the cached columns and the
/// real membership stay consistent under concurrent writers.
pub async fn recompute_folder_last_media_files<'e, 'c: 'e, E>(
args: RecomputeFolderLastMediaFilesArgs<'e, 'c, E>,
) -> Result<(), sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let folder_token_str = args.folder_token.as_str();
sqlx::query!(
r#"
UPDATE folders
SET
maybe_last_media_file_token_1 = (
SELECT mf.token
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
ORDER BY fmf.id DESC
LIMIT 1 OFFSET 0
),
maybe_last_media_file_token_2 = (
SELECT mf.token
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
ORDER BY fmf.id DESC
LIMIT 1 OFFSET 1
),
maybe_last_media_file_token_3 = (
SELECT mf.token
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
ORDER BY fmf.id DESC
LIMIT 1 OFFSET 2
),
maybe_last_media_file_token_4 = (
SELECT mf.token
FROM folder_media_files fmf
JOIN media_files mf
ON mf.token = fmf.media_file_token
WHERE fmf.folder_token = ?
AND mf.user_deleted_at IS NULL
AND mf.mod_deleted_at IS NULL
ORDER BY fmf.id DESC
LIMIT 1 OFFSET 3
)
WHERE token = ?
"#,
folder_token_str,
folder_token_str,
folder_token_str,
folder_token_str,
folder_token_str,
)
.execute(args.mysql_executor)
.await?;
Ok(())
}
@@ -0,0 +1,3 @@
pub mod folder;
pub mod media_files;
pub mod subfolder;
@@ -0,0 +1,50 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct BulkClearParentFolderArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub child_tokens: &'e [FolderToken],
pub from_parent_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Clear `maybe_parent_folder_token` (set to NULL) on every owned, live
/// folder whose token is in `child_tokens` AND whose current parent is
/// `from_parent_token`. The parent guard makes the operation safe to run
/// idempotently — it won't accidentally unparent a folder that's a child
/// of a different parent.
pub async fn bulk_clear_parent_folder<'e, 'c: 'e, E>(
args: BulkClearParentFolderArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.child_tokens.is_empty() {
return Ok(0);
}
let mut builder = QueryBuilder::<MySql>::new(
"UPDATE folders SET maybe_parent_folder_token = NULL WHERE owner_user_token = ",
);
builder.push_bind(args.owner_user_token.as_str());
builder.push(" AND maybe_deleted_at IS NULL AND maybe_parent_folder_token = ");
builder.push_bind(args.from_parent_token.as_str());
builder.push(" AND token IN (");
let mut separated = builder.separated(", ");
for token in args.child_tokens {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let result = builder.build().execute(args.mysql_executor).await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,67 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct BulkSetParentFolderArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub child_tokens: &'e [FolderToken],
pub new_parent_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Set `maybe_parent_folder_token = new_parent_token` on every owned,
/// live folder whose token is in `child_tokens` AND that isn't the parent
/// itself (a folder can't be its own parent). Returns the number of rows
/// whose value actually changed.
///
/// Self-referential parenting is prevented at two layers:
/// * Rust-side: the parent is filtered out of the candidate list
/// before the SQL is built.
/// * SQL-side: the WHERE clause includes `AND token != new_parent_token`
/// as a defense-in-depth guard.
pub async fn bulk_set_parent_folder<'e, 'c: 'e, E>(
args: BulkSetParentFolderArgs<'e, 'c, E>,
) -> Result<u64, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
// Drop the parent from the candidate list. This makes the function
// self-protecting — callers don't have to remember to filter — and
// pairs with the SQL `token != new_parent_token` guard below.
let new_parent_str = args.new_parent_token.as_str();
let filtered_children: Vec<&FolderToken> = args.child_tokens
.iter()
.filter(|t| t.as_str() != new_parent_str)
.collect();
if filtered_children.is_empty() {
return Ok(0);
}
let mut builder = QueryBuilder::<MySql>::new(
"UPDATE folders SET maybe_parent_folder_token = ",
);
builder.push_bind(new_parent_str);
builder.push(" WHERE owner_user_token = ");
builder.push_bind(args.owner_user_token.as_str());
builder.push(" AND maybe_deleted_at IS NULL AND token != ");
builder.push_bind(new_parent_str);
builder.push(" AND token IN (");
let mut separated = builder.separated(", ");
for token in filtered_children {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let result = builder.build().execute(args.mysql_executor).await?;
Ok(result.rows_affected())
}
@@ -0,0 +1,51 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder, Row};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::users::UserToken;
pub struct FilterExistingOwnedFolderTokensArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub candidate_tokens: &'e [FolderToken],
pub owner_user_token: &'e UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Given an input set of candidate folder tokens, return only those that
/// currently exist, are owned by the given user, and are not soft-deleted.
/// Order of the result is unspecified.
///
/// Uses runtime `QueryBuilder` because the IN-list size is dynamic and the
/// macro form can't express that.
pub async fn filter_existing_owned_folder_tokens<'e, 'c: 'e, E>(
args: FilterExistingOwnedFolderTokensArgs<'e, 'c, E>,
) -> Result<Vec<FolderToken>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.candidate_tokens.is_empty() {
return Ok(Vec::new());
}
let mut builder = QueryBuilder::<MySql>::new(
"SELECT token FROM folders WHERE owner_user_token = ",
);
builder.push_bind(args.owner_user_token.as_str());
builder.push(" AND maybe_deleted_at IS NULL AND token IN (");
let mut separated = builder.separated(", ");
for token in args.candidate_tokens {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let rows = builder.build().fetch_all(args.mysql_executor).await?;
Ok(rows.into_iter()
.map(|row| FolderToken::new(row.get::<String, _>(0)))
.collect())
}
@@ -0,0 +1,143 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::folders::folder::folder_row::FolderRow;
pub struct ListSubfoldersArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub parent_folder_token: &'e FolderToken,
pub owner_user_token: &'e UserToken,
pub maybe_cursor_id: Option<u64>,
pub limit: u32,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Paginated list of live folders whose `maybe_parent_folder_token` is
/// the given parent. Scoped to the owner to avoid leaking siblings of
/// folders owned by another user. `is_orphaned` is always `false` here:
/// by definition, the parent matched so it exists.
pub async fn list_subfolders<'e, 'c: 'e, E>(
args: ListSubfoldersArgs<'e, 'c, E>,
) -> Result<Vec<FolderRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let limit = args.limit as i64;
let rows = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query!(
r#"
SELECT
f.id as `id: u64`,
f.token as `token: FolderToken`,
f.name,
f.owner_user_token as `owner_user_token: UserToken`,
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
f.maybe_color_code,
f.has_star as `has_star: bool`,
f.created_at,
f.updated_at
FROM folders f
WHERE f.maybe_parent_folder_token = ?
AND f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
AND f.id < ?
ORDER BY f.id DESC
LIMIT ?
"#,
args.parent_folder_token.as_str(),
args.owner_user_token.as_str(),
cursor_id,
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderRow {
id: r.id,
token: r.token,
name: r.name,
owner_user_token: r.owner_user_token,
maybe_parent_folder_token: r.maybe_parent_folder_token,
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
maybe_color_code: r.maybe_color_code,
has_star: r.has_star,
created_at: r.created_at,
updated_at: r.updated_at,
is_orphaned: false,
})
.collect::<Vec<_>>()
}
None => {
sqlx::query!(
r#"
SELECT
f.id as `id: u64`,
f.token as `token: FolderToken`,
f.name,
f.owner_user_token as `owner_user_token: UserToken`,
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
f.maybe_color_code,
f.has_star as `has_star: bool`,
f.created_at,
f.updated_at
FROM folders f
WHERE f.maybe_parent_folder_token = ?
AND f.owner_user_token = ?
AND f.maybe_deleted_at IS NULL
ORDER BY f.id DESC
LIMIT ?
"#,
args.parent_folder_token.as_str(),
args.owner_user_token.as_str(),
limit,
)
.fetch_all(args.mysql_executor)
.await?
.into_iter()
.map(|r| FolderRow {
id: r.id,
token: r.token,
name: r.name,
owner_user_token: r.owner_user_token,
maybe_parent_folder_token: r.maybe_parent_folder_token,
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
maybe_color_code: r.maybe_color_code,
has_star: r.has_star,
created_at: r.created_at,
updated_at: r.updated_at,
is_orphaned: false,
})
.collect::<Vec<_>>()
}
};
Ok(rows)
}
@@ -0,0 +1,4 @@
pub mod bulk_clear_parent_folder;
pub mod bulk_set_parent_folder;
pub mod filter_existing_owned_folder_tokens;
pub mod list_subfolders;
@@ -6,6 +6,7 @@ use sqlx::{MySql, MySqlPool};
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -30,6 +31,8 @@ pub struct BeebleJobDetails {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
pub maybe_platform_type: Option<PlatformType>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -53,6 +56,8 @@ struct RawJobRecord {
maybe_prompt_token: Option<PromptToken>,
maybe_debug_log_event_token: Option<DebugLogEventToken>,
maybe_platform_type: Option<PlatformType>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -87,6 +92,8 @@ SELECT
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,
jobs.created_at,
jobs.updated_at
@@ -128,6 +135,7 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<BeebleJobDe
creator_ip_address: record.creator_ip_address,
maybe_prompt_token: record.maybe_prompt_token,
maybe_debug_log_event_token: record.maybe_debug_log_event_token,
maybe_platform_type: record.maybe_platform_type,
created_at: record.created_at,
updated_at: record.updated_at,
})
@@ -9,6 +9,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -46,6 +47,9 @@ pub struct InsertGenericInferenceForBeebleWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
/// Override the initial job status. Defaults to `Pending` when `None`.
@@ -83,6 +87,7 @@ pub async fn insert_generic_inference_job_for_beeble_queue_with_apriori_job_toke
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -26,6 +26,7 @@ use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobTyp
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -71,6 +72,9 @@ pub(crate) struct InsertGenericInferenceJobForProviderArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
@@ -114,6 +118,8 @@ pub(crate) async fn insert_generic_inference_job_for_provider<'e, 'c: 'e, E>(
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
@@ -6,6 +6,7 @@ use sqlx::{MySql, MySqlPool};
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -32,6 +33,8 @@ pub struct FalJobDetails {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_debug_log_event_token: Option<DebugLogEventToken>,
pub maybe_platform_type: Option<PlatformType>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -55,6 +58,8 @@ struct RawJobRecord {
maybe_prompt_token: Option<PromptToken>,
maybe_debug_log_event_token: Option<DebugLogEventToken>,
maybe_platform_type: Option<PlatformType>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -93,6 +98,8 @@ SELECT
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_debug_log_event_token as `maybe_debug_log_event_token: tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,
jobs.created_at,
jobs.updated_at
@@ -134,6 +141,7 @@ fn raw_record_to_public_result(record: RawJobRecord) -> AnyhowResult<FalJobDetai
creator_ip_address: record.creator_ip_address,
maybe_prompt_token: record.maybe_prompt_token,
maybe_debug_log_event_token: record.maybe_debug_log_event_token,
maybe_platform_type: record.maybe_platform_type,
created_at: record.created_at,
updated_at: record.updated_at,
})
@@ -2,6 +2,7 @@ use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -41,6 +42,9 @@ pub struct InsertGenericInferenceForFalArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub mysql_executor: E,
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
@@ -66,6 +70,7 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -5,6 +5,7 @@ use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -41,6 +42,9 @@ pub struct InsertGenericInferenceForFalMockFailureArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
pub maybe_failure_reason: Option<&'e str>,
@@ -69,6 +73,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -9,6 +9,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -49,6 +50,9 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Override the initial job status. Defaults to `Pending` when `None`.
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
@@ -105,6 +109,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -33,6 +34,7 @@ where
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -60,6 +62,7 @@ where
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -27,6 +28,8 @@ pub struct PendingGmiCloudJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -39,6 +42,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal GmiCloud jobs that have an associated request_id.
@@ -54,7 +58,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -90,6 +95,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -33,6 +34,7 @@ where
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -60,6 +62,7 @@ where
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -2,6 +2,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -25,6 +26,8 @@ pub struct PendingGrokApiJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -37,6 +40,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal Grok (xAI) API jobs that have an associated request_id.
@@ -52,7 +56,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -87,6 +92,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -63,6 +63,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_character_with_aprior
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -50,6 +51,9 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
@@ -126,6 +130,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_queue_with_apriori_jo
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -26,6 +27,8 @@ pub struct PendingSeedance2ProJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -38,6 +41,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal Seedance2Pro video jobs that have an associated order_id.
@@ -57,7 +61,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -94,6 +99,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -8,6 +8,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -45,6 +46,9 @@ pub struct InsertGenericInferenceForWorldlabsWithAprioriJobTokenArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -71,6 +75,7 @@ pub async fn insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_t
maybe_avt_token: args.maybe_avt_token,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -3,6 +3,7 @@ use log::warn;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -27,6 +28,8 @@ pub struct PendingWorldlabsJob {
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
pub maybe_platform_type: Option<PlatformType>,
}
#[derive(Debug, Default)]
@@ -39,6 +42,7 @@ struct RawRecord {
creator_set_visibility: Visibility,
maybe_prompt_token: Option<PromptToken>,
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
maybe_platform_type: Option<PlatformType>,
}
/// Returns all non-terminal WorldLabs jobs that have an associated operation_id.
@@ -54,7 +58,8 @@ SELECT
jobs.creator_ip_address,
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,
jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`
FROM generic_inference_jobs as jobs
@@ -90,6 +95,7 @@ LIMIT 25000
creator_set_visibility: record.creator_set_visibility,
maybe_prompt_token: record.maybe_prompt_token,
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
maybe_platform_type: record.maybe_platform_type,
})
})
.collect();
@@ -27,6 +27,7 @@ use enums::by_table::generic_inference_jobs::inference_job_product_category::Inf
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -76,6 +77,9 @@ pub(crate) struct InsertFullGenericInferenceJobRecordArgs<'e, 'c, E>
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
pub priority_level: u8,
pub requires_keepalive: bool,
pub max_duration_seconds: i32,
@@ -157,6 +161,8 @@ SET
creator_ip_address = ?,
creator_set_visibility = ?,
platform_type = ?,
priority_level = ?,
is_keepalive_required = ?,
max_duration_seconds = ?,
@@ -203,6 +209,8 @@ SET
args.creator_ip_address,
args.creator_set_visibility.to_str(),
args.maybe_platform_type.map(|p| p.to_str()),
args.priority_level,
args.requires_keepalive,
args.max_duration_seconds,
@@ -121,6 +121,8 @@ pub async fn insert_generic_inference_job(args: InsertGenericInferenceArgs<'_>)
maybe_routing_tag: args.maybe_routing_tag,
maybe_platform_type: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -10,6 +10,7 @@ use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginM
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
@@ -92,6 +93,10 @@ pub struct InsertArgs<'a> {
/// If provided, the third-party provider that generated this file.
pub maybe_generation_provider: Option<GenerationProvider>,
/// The platform the creating request came from, inferred from its User-Agent.
/// For inference results, this is copied from the originating job.
pub maybe_platform_type: Option<PlatformType>,
// Cover image (e.g. thumbnail for 3D splats)
pub maybe_cover_image_media_file_token: Option<&'a MediaFileToken>,
@@ -204,6 +209,8 @@ pub async fn insert_media_file_generic(
maybe_generation_provider = ?,
platform_type = ?,
maybe_cover_image_media_file_token = ?,
maybe_mod_user_token = ?,
@@ -262,6 +269,8 @@ pub async fn insert_media_file_generic(
maybe_generation_provider_str,
args.maybe_platform_type.map(|p| p.to_str()),
args.maybe_cover_image_media_file_token.map(|t| t.as_str()),
args.maybe_mod_user_token,
@@ -121,6 +121,7 @@ pub async fn insert_media_file_generic_from_job(
maybe_title: args.maybe_title,
maybe_video_encoding: args.maybe_video_encoding,
maybe_generation_provider: None,
maybe_platform_type: None,
maybe_cover_image_media_file_token: None,
media_class: args.media_class,
media_type: args.media_type,
@@ -10,6 +10,7 @@ use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use sqlx::MySqlPool;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -82,6 +83,10 @@ pub struct MediaFileInsertBuilder {
// Generation details
maybe_generation_provider: Option<GenerationProvider>,
// The platform the creating request came from. For inference results, this
// is copied from the originating generic_inference_jobs row.
maybe_platform_type: Option<PlatformType>,
}
impl MediaFileInsertBuilder {
@@ -116,6 +121,7 @@ impl MediaFileInsertBuilder {
public_bucket_directory_hash: None,
maybe_prompt_token: None,
maybe_generation_provider: None,
maybe_platform_type: None,
}
}
@@ -272,6 +278,11 @@ impl MediaFileInsertBuilder {
self
}
pub fn maybe_platform_type(mut self, maybe_platform_type: Option<PlatformType>) -> Self {
self.maybe_platform_type = maybe_platform_type;
self
}
// TODO(bt,2025-04-26): Other connector options.
pub async fn insert_pool(self, mysql_pool: &MySqlPool) -> Result<MediaFileToken, MediaFileInsertBuilderError> {
let media_file_type = self.media_file_type
@@ -341,6 +352,7 @@ impl MediaFileInsertBuilder {
maybe_creator_category_synthetic_id_category: IdCategory::FileUpload, // TODO: Remove this
maybe_extra_media_info: None, // TODO
maybe_generation_provider: self.maybe_generation_provider,
maybe_platform_type: self.maybe_platform_type,
maybe_cover_image_media_file_token: self.maybe_cover_image_media_file_token.as_ref(),
is_generated_on_prem: false, // TODO
generated_by_worker: None, // TODO
@@ -112,6 +112,7 @@ pub async fn insert_media_file_from_studio_scene_render(
maybe_mod_user_token: None,
maybe_engine_category: None,
maybe_generation_provider: None,
maybe_platform_type: None,
maybe_cover_image_media_file_token: None,
}).await?;
@@ -0,0 +1,91 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql, QueryBuilder};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::media_files::MediaFileToken;
/// Minimal columns the folder-thumbnail builder needs from `media_files`.
#[derive(Debug, Clone)]
pub struct MediaFileThumbnailRow {
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
}
pub struct BatchGetMediaFileThumbnailsByTokensArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub candidate_tokens: &'e [MediaFileToken],
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Look up the bucket-path / class / type for each of the given media
/// file tokens. Silently drops rows that don't exist or are soft-deleted.
/// Caller is responsible for de-duplicating the input set if needed; the
/// SQL doesn't care about duplicates.
///
/// Uses runtime `QueryBuilder` because the IN-list size is dynamic.
pub async fn batch_get_media_file_thumbnails_by_tokens<'e, 'c: 'e, E>(
args: BatchGetMediaFileThumbnailsByTokensArgs<'e, 'c, E>,
) -> Result<Vec<MediaFileThumbnailRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
if args.candidate_tokens.is_empty() {
return Ok(Vec::new());
}
let mut builder = QueryBuilder::<MySql>::new(
"SELECT \
token, \
media_class, \
media_type, \
public_bucket_directory_hash, \
maybe_public_bucket_prefix, \
maybe_public_bucket_extension \
FROM media_files \
WHERE user_deleted_at IS NULL \
AND mod_deleted_at IS NULL \
AND token IN (",
);
let mut separated = builder.separated(", ");
for token in args.candidate_tokens {
separated.push_bind(token.as_str());
}
separated.push_unseparated(")");
let rows = builder
.build_query_as::<RawRow>()
.fetch_all(args.mysql_executor)
.await?;
Ok(rows.into_iter().map(|r| MediaFileThumbnailRow {
token: MediaFileToken::new(r.token),
media_class: MediaFileClass::from_str(&r.media_class).unwrap_or(MediaFileClass::Image),
media_type: MediaFileType::from_str(&r.media_type).unwrap_or(MediaFileType::Image),
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
}).collect())
}
/// Internal row shape used by the dynamic `QueryBuilder` call. We hand-roll
/// the deserialization here because `QueryBuilder` can't ride the
/// compile-time type checks of `sqlx::query!`/`query_as!`.
#[derive(Debug, sqlx::FromRow)]
struct RawRow {
token: String,
media_class: String,
media_type: String,
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
}
@@ -0,0 +1,62 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
#[derive(Debug, Clone)]
pub struct MediaFileForCoverCheck {
pub maybe_creator_user_token: Option<UserToken>,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub maybe_cover_image_media_file_token: Option<MediaFileToken>,
}
pub struct LookupMediaFileForCoverCheckArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
pub media_file_token: &'e MediaFileToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Minimal lookup of a media file for cover-image authorization +
/// classification. Returns the creator user token (for ownership checks),
/// the media class and type (to decide whether the file can be used as a
/// cover directly), and the file's own self-referential cover-image token
/// (for the "use the cover's cover" fallback). Excludes soft-deleted rows.
pub async fn lookup_media_file_for_cover_check<'e, 'c: 'e, E>(
args: LookupMediaFileForCoverCheckArgs<'e, 'c, E>,
) -> Result<Option<MediaFileForCoverCheck>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let result = sqlx::query!(
r#"
SELECT
maybe_creator_user_token as `maybe_creator_user_token: UserToken`,
media_class as `media_class: MediaFileClass`,
media_type as `media_type: MediaFileType`,
maybe_cover_image_media_file_token as `maybe_cover_image_media_file_token: MediaFileToken`
FROM media_files
WHERE token = ?
AND user_deleted_at IS NULL
AND mod_deleted_at IS NULL
LIMIT 1
"#,
args.media_file_token.as_str(),
)
.fetch_optional(args.mysql_executor)
.await?;
Ok(result.map(|r| MediaFileForCoverCheck {
maybe_creator_user_token: r.maybe_creator_user_token,
media_class: r.media_class,
media_type: r.media_type,
maybe_cover_image_media_file_token: r.maybe_cover_image_media_file_token,
}))
}
@@ -1,4 +1,6 @@
pub mod batch_get_media_file_thumbnails_by_tokens;
pub mod batch_get_media_files;
pub mod batch_get_media_files_by_tokens;
pub mod get_media_file;
pub mod get_media_file_for_inference;
pub mod get_media_file_for_inference;
pub mod lookup_media_file_for_cover_check;
@@ -13,6 +13,7 @@ pub mod database_time;
pub mod email_sender_jobs;
pub mod entity_stats;
pub mod featured_items;
pub mod folders;
pub mod generic_download;
pub mod generic_inference;
pub mod generic_synthetic_ids;
@@ -1,6 +1,7 @@
pub mod user;
pub mod user_badges;
pub mod user_bookmarks;
pub mod user_email_changes;
pub mod user_password_resets;
pub mod user_profiles;
pub mod user_ratings;
@@ -0,0 +1,59 @@
use std::marker::PhantomData;
use sqlx::{Executor, MySql};
use tokens::tokens::users::UserToken;
pub struct InsertUserEmailChangeArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
/// The user whose email was changed.
pub user_token: &'e UserToken,
/// The email address before the change.
pub old_email: &'e str,
/// The email address after the change.
pub new_email: &'e str,
/// The IP address the change was made from.
pub ip_address: &'e str,
/// The user that performed the change, if known. `None` when the change
/// was performed by the user themselves, by an automated process, or in
/// any other context where there is no acting user.
pub maybe_changed_by_user_token: Option<&'e UserToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// Insert a single `user_email_changes` row recording an email-address change.
pub async fn insert_user_email_change<'e, 'c: 'e, E>(
args: InsertUserEmailChangeArgs<'e, 'c, E>,
) -> Result<(), sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
sqlx::query!(
r#"
INSERT INTO user_email_changes
SET
user_token = ?,
old_email = ?,
new_email = ?,
ip_address = ?,
maybe_changed_by_user_token = ?
"#,
args.user_token.as_str(),
args.old_email,
args.new_email,
args.ip_address,
args.maybe_changed_by_user_token.map(|t| t.as_str()),
)
.execute(args.mysql_executor)
.await?;
Ok(())
}
@@ -0,0 +1,208 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::{Executor, MySql};
use tokens::tokens::users::UserToken;
pub struct ListUserEmailChangesForUserArgs<'e, 'c, E>
where
E: 'e + Executor<'c, Database = MySql>,
{
/// The user whose email-change history is being listed.
pub user_token: &'e UserToken,
/// When `Some`, only return rows with `id < maybe_cursor_id`. Used to
/// page backward through the descending-by-id list.
pub maybe_cursor_id: Option<u64>,
/// Maximum number of rows to return.
pub limit: u32,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
#[derive(Debug)]
pub struct UserEmailChangeRow {
pub id: u64,
pub old_email: String,
pub new_email: String,
pub ip_address: String,
pub created_at: DateTime<Utc>,
/// The user whose email was changed (the subject of the audit row).
pub user: UserDisplay,
/// The user who performed the change. `None` for self-service changes
/// (where `user_email_changes.maybe_changed_by_user_token` is `NULL`) or
/// when the referenced user row has been hard-deleted.
pub maybe_changed_by_user: Option<UserDisplay>,
}
/// Denormalized display fields for a `users` row, joined into a query result.
#[derive(Debug)]
pub struct UserDisplay {
pub token: UserToken,
pub username: String,
pub display_name: String,
pub gravatar_hash: String,
}
#[derive(Debug)]
struct RawUserEmailChangeRow {
id: u64,
old_email: String,
new_email: String,
ip_address: String,
created_at: DateTime<Utc>,
user_token: UserToken,
user_username: String,
user_display_name: String,
user_gravatar_hash: String,
maybe_changed_by_user_token: Option<UserToken>,
maybe_changed_by_user_username: Option<String>,
maybe_changed_by_user_display_name: Option<String>,
maybe_changed_by_user_gravatar_hash: Option<String>,
}
/// Return `user_email_changes` rows for the given user, newest first,
/// capped at `limit`. When `maybe_cursor_id` is set, only rows with `id`
/// strictly less than the cursor are returned — pass the last id from the
/// previous page to walk backwards through history.
///
/// Joins `users` twice to denormalize the token, username, display name,
/// and gravatar hash for both the subject (`user_token`) and the acting
/// user (`maybe_changed_by_user_token`). The actor join is a LEFT JOIN so
/// rows where there is no acting user (e.g. self-service changes) still
/// come back.
pub async fn list_user_email_changes_for_user<'e, 'c: 'e, E>(
args: ListUserEmailChangesForUserArgs<'e, 'c, E>,
) -> Result<Vec<UserEmailChangeRow>, sqlx::Error>
where
E: 'e + Executor<'c, Database = MySql>,
{
let limit = args.limit as i64;
// Two query branches because sqlx::query_as! can't accept conditional
// WHERE clauses at the macro level. `args.mysql_executor` is moved into
// exactly one arm at runtime — Rust permits this since the arms are
// mutually exclusive.
let rows = match args.maybe_cursor_id {
Some(cursor_id) => {
sqlx::query_as!(
RawUserEmailChangeRow,
r#"
SELECT
uec.id as `id: u64`,
uec.old_email,
uec.new_email,
uec.ip_address,
uec.created_at,
u_subject.token as `user_token: tokens::tokens::users::UserToken`,
u_subject.username as user_username,
u_subject.display_name as user_display_name,
u_subject.email_gravatar_hash as user_gravatar_hash,
u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,
u_changer.username as `maybe_changed_by_user_username?`,
u_changer.display_name as `maybe_changed_by_user_display_name?`,
u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`
FROM user_email_changes AS uec
JOIN users AS u_subject
ON uec.user_token = u_subject.token
LEFT JOIN users AS u_changer
ON uec.maybe_changed_by_user_token = u_changer.token
WHERE uec.user_token = ?
AND uec.id < ?
ORDER BY uec.id DESC
LIMIT ?
"#,
args.user_token.as_str(),
cursor_id,
limit,
)
.fetch_all(args.mysql_executor)
.await?
}
None => {
sqlx::query_as!(
RawUserEmailChangeRow,
r#"
SELECT
uec.id as `id: u64`,
uec.old_email,
uec.new_email,
uec.ip_address,
uec.created_at,
u_subject.token as `user_token: tokens::tokens::users::UserToken`,
u_subject.username as user_username,
u_subject.display_name as user_display_name,
u_subject.email_gravatar_hash as user_gravatar_hash,
u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,
u_changer.username as `maybe_changed_by_user_username?`,
u_changer.display_name as `maybe_changed_by_user_display_name?`,
u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`
FROM user_email_changes AS uec
JOIN users AS u_subject
ON uec.user_token = u_subject.token
LEFT JOIN users AS u_changer
ON uec.maybe_changed_by_user_token = u_changer.token
WHERE uec.user_token = ?
ORDER BY uec.id DESC
LIMIT ?
"#,
args.user_token.as_str(),
limit,
)
.fetch_all(args.mysql_executor)
.await?
}
};
let results = rows.into_iter().map(|row| {
let user = UserDisplay {
token: row.user_token,
username: row.user_username,
display_name: row.user_display_name,
gravatar_hash: row.user_gravatar_hash,
};
// The four actor columns come from the same LEFT JOIN row, so they are
// either all `Some` (matched user row) or all `None` (no actor token or
// the user row was hard-deleted) together.
let maybe_changed_by_user = match (
row.maybe_changed_by_user_token,
row.maybe_changed_by_user_username,
row.maybe_changed_by_user_display_name,
row.maybe_changed_by_user_gravatar_hash,
) {
(Some(token), Some(username), Some(display_name), Some(gravatar_hash)) => Some(UserDisplay {
token,
username,
display_name,
gravatar_hash,
}),
_ => None,
};
UserEmailChangeRow {
id: row.id,
old_email: row.old_email,
new_email: row.new_email,
ip_address: row.ip_address,
created_at: row.created_at,
user,
maybe_changed_by_user,
}
}).collect();
Ok(results)
}
@@ -0,0 +1,2 @@
pub mod insert_user_email_change;
pub mod list_user_email_changes_for_user;

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