From b91002bb1e6eb6abe28a36a9b71857246598bd57 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 22 Jun 2026 18:04:40 -0400 Subject: [PATCH] PM-28: Write endpoints --- ...a5115b0b3e2f063979d13cd8cb11330c7d382.json | 21 + ...860d3dd91b5749ccaa6a6e8cd65764d720905.json | 21 + ...4081d97244d658f169b8e0bda21d40f4416dd.json | 38 + ...bfd4c3f0918190f4ecd265314b213451bcf77.json | 21 + ...5cc3dd7b39e688652f305b957e61e8c18b661.json | 26 + ...24b6695604f173e09f05445add90eee55dae2.json | 21 + ...623d1eb6cc6e95287f5c9c8e890fd727a056e.json | 21 + ...253653f0b2af89db76077142a71cc7f1643d0.json | 21 + ...45677fbe7a38b9ed034d70d5aea7be3603b9c.json | 50 + ...5e34777a5ab609d8eb454836d676800ff0511.json | 12 - ...6d116f2dec107de7fdba8cc8644ebbc6d0ae3.json | 38 + ...25dad4abd7cba9b604fe2a25d77bcf3d5f1af.json | 12 - ...9fbeb50c24b15248ac8516ede592e68b09523.json | 12 - ...e13b7f1192296f332354b9e174150fd5e89ac.json | 50 + ...4b59d1c25cf1024571297cbe32c3efb86c03d.json | 12 - ...1e6e1f9ae120827a7abf0d150f1a37936fd4f.json | 12 + ...38849f8350117fb65f773f9429709dd588a09.json | 12 + ...b33277438ac7fd538b9e03334988dbd014fac.json | 21 + ...ce22044ab9f955f59b1d57ca2a03837aa0728.json | 12 + ...b693fb60120cfed020b300efe1f55e1e9564c.json | 50 + ...4d5f65ae2e0b6ac708f00bb750974412c4fb2.json | 12 + ...f948f77dd5faa65c0b75664142a4b888986da.json | 12 + Cargo.lock | 32 + Cargo.toml | 13 +- ...pace_polymodel_actor_bootstrapProfile.json | 42 + .../space_polymodel_graph_createLike.json | 48 + .../space_polymodel_graph_createSave.json | 52 + .../space_polymodel_graph_createTag.json | 54 + .../space_polymodel_graph_deleteLike.json | 44 + .../space_polymodel_graph_deleteSave.json | 44 + .../space_polymodel_graph_deleteTag.json | 43 + .../space_polymodel_library_deleteThing.json | 46 + .../space_polymodel_library_publishThing.json | 296 +++ .../space_polymodel_library_stageFile.json | 41 + .../space_polymodel_library_updateThing.json | 272 +++ .../src/space_polymodel/actor.rs | 1 + .../actor/bootstrap_profile.rs | 161 ++ .../src/space_polymodel/graph.rs | 6 + .../src/space_polymodel/graph/create_like.rs | 189 ++ .../src/space_polymodel/graph/create_save.rs | 206 ++ .../src/space_polymodel/graph/create_tag.rs | 224 ++ .../src/space_polymodel/graph/delete_like.rs | 73 + .../src/space_polymodel/graph/delete_save.rs | 73 + .../src/space_polymodel/graph/delete_tag.rs | 187 ++ .../src/space_polymodel/library.rs | 4 + .../space_polymodel/library/delete_thing.rs | 187 ++ .../space_polymodel/library/publish_thing.rs | 2003 +++++++++++++++++ .../src/space_polymodel/library/stage_file.rs | 166 ++ .../space_polymodel/library/update_thing.rs | 1663 ++++++++++++++ justfile | 6 +- lexicons/actor/bootstrapProfile.json | 26 + lexicons/graph/createLike.json | 32 + lexicons/graph/createSave.json | 33 + lexicons/graph/createTag.json | 33 + lexicons/graph/deleteLike.json | 31 + lexicons/graph/deleteSave.json | 31 + lexicons/graph/deleteTag.json | 31 + lexicons/library/deleteThing.json | 31 + lexicons/library/publishThing.json | 96 + lexicons/library/stageFile.json | 26 + lexicons/library/updateThing.json | 83 + migrations/003_upload_staging.sql | 21 + migrations/004_social_record_cids.sql | 56 + src/appview/error.rs | 10 + src/appview/mod.rs | 34 +- src/appview/state.rs | 7 + src/appview/tests.rs | 53 +- src/appview/writes.rs | 1908 ++++++++++++++++ src/indexing/projection.rs | 44 +- 69 files changed, 9202 insertions(+), 67 deletions(-) create mode 100644 .sqlx/query-05e2314ba1bdc25c78d9468e6a7a5115b0b3e2f063979d13cd8cb11330c7d382.json create mode 100644 .sqlx/query-0d80344b5e78f5c0d5de9cf752b860d3dd91b5749ccaa6a6e8cd65764d720905.json create mode 100644 .sqlx/query-0ed97286ed1a532f11a124fbb744081d97244d658f169b8e0bda21d40f4416dd.json create mode 100644 .sqlx/query-1133e593e6973e28ad03b0297f3bfd4c3f0918190f4ecd265314b213451bcf77.json create mode 100644 .sqlx/query-2094180851ce8e023590116d2bf5cc3dd7b39e688652f305b957e61e8c18b661.json create mode 100644 .sqlx/query-4486bc1f0e41e6cf884dddaa52c24b6695604f173e09f05445add90eee55dae2.json create mode 100644 .sqlx/query-563c3c23ec87f048b811625df4d623d1eb6cc6e95287f5c9c8e890fd727a056e.json create mode 100644 .sqlx/query-6b48422566a6ed1221ec83e82b8253653f0b2af89db76077142a71cc7f1643d0.json create mode 100644 .sqlx/query-7bdc04619643992941c43a742f045677fbe7a38b9ed034d70d5aea7be3603b9c.json delete mode 100644 .sqlx/query-8b02bdc1204d6e4afd30d29aa605e34777a5ab609d8eb454836d676800ff0511.json create mode 100644 .sqlx/query-993cd27061ba88a858f7021f8e86d116f2dec107de7fdba8cc8644ebbc6d0ae3.json delete mode 100644 .sqlx/query-af25cf6fdd1ddc048069a952e8a25dad4abd7cba9b604fe2a25d77bcf3d5f1af.json delete mode 100644 .sqlx/query-b4a4296cbfee1f54b3e025416499fbeb50c24b15248ac8516ede592e68b09523.json create mode 100644 .sqlx/query-bc2637c2b2950f580859536b190e13b7f1192296f332354b9e174150fd5e89ac.json delete mode 100644 .sqlx/query-c9ef48617a88d2241b9959825184b59d1c25cf1024571297cbe32c3efb86c03d.json create mode 100644 .sqlx/query-cb4c8e4f4748a49049c4e942e6c1e6e1f9ae120827a7abf0d150f1a37936fd4f.json create mode 100644 .sqlx/query-cd961657cb3d0a41d2e6b503d4038849f8350117fb65f773f9429709dd588a09.json create mode 100644 .sqlx/query-ce65448db03ba94432bb911c797b33277438ac7fd538b9e03334988dbd014fac.json create mode 100644 .sqlx/query-db731da68101b9edd9662d51f7fce22044ab9f955f59b1d57ca2a03837aa0728.json create mode 100644 .sqlx/query-e16314e2226f9cf2334ca747403b693fb60120cfed020b300efe1f55e1e9564c.json create mode 100644 .sqlx/query-e9573839c7d4c31f3960e6415774d5f65ae2e0b6ac708f00bb750974412c4fb2.json create mode 100644 .sqlx/query-f727bf890c756a13373922ed6f5f948f77dd5faa65c0b75664142a4b888986da.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_actor_bootstrapProfile.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_createLike.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_createSave.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_createTag.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_deleteLike.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_deleteSave.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_graph_deleteTag.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_library_deleteThing.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_library_publishThing.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_library_stageFile.json create mode 100644 crates/polymodel-api/lexicons/space_polymodel_library_updateThing.json create mode 100644 crates/polymodel-api/src/space_polymodel/actor/bootstrap_profile.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/create_like.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/create_save.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/create_tag.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/delete_like.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/delete_save.rs create mode 100644 crates/polymodel-api/src/space_polymodel/graph/delete_tag.rs create mode 100644 crates/polymodel-api/src/space_polymodel/library/delete_thing.rs create mode 100644 crates/polymodel-api/src/space_polymodel/library/publish_thing.rs create mode 100644 crates/polymodel-api/src/space_polymodel/library/stage_file.rs create mode 100644 crates/polymodel-api/src/space_polymodel/library/update_thing.rs create mode 100644 lexicons/actor/bootstrapProfile.json create mode 100644 lexicons/graph/createLike.json create mode 100644 lexicons/graph/createSave.json create mode 100644 lexicons/graph/createTag.json create mode 100644 lexicons/graph/deleteLike.json create mode 100644 lexicons/graph/deleteSave.json create mode 100644 lexicons/graph/deleteTag.json create mode 100644 lexicons/library/deleteThing.json create mode 100644 lexicons/library/publishThing.json create mode 100644 lexicons/library/stageFile.json create mode 100644 lexicons/library/updateThing.json create mode 100644 migrations/003_upload_staging.sql create mode 100644 migrations/004_social_record_cids.sql create mode 100644 src/appview/writes.rs diff --git a/.sqlx/query-05e2314ba1bdc25c78d9468e6a7a5115b0b3e2f063979d13cd8cb11330c7d382.json b/.sqlx/query-05e2314ba1bdc25c78d9468e6a7a5115b0b3e2f063979d13cd8cb11330c7d382.json new file mode 100644 index 0000000..7b9bc48 --- /dev/null +++ b/.sqlx/query-05e2314ba1bdc25c78d9468e6a7a5115b0b3e2f063979d13cd8cb11330c7d382.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM models WHERE uri = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM models WHERE uri = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "05e2314ba1bdc25c78d9468e6a7a5115b0b3e2f063979d13cd8cb11330c7d382" +} diff --git a/.sqlx/query-0d80344b5e78f5c0d5de9cf752b860d3dd91b5749ccaa6a6e8cd65764d720905.json b/.sqlx/query-0d80344b5e78f5c0d5de9cf752b860d3dd91b5749ccaa6a6e8cd65764d720905.json new file mode 100644 index 0000000..0769f66 --- /dev/null +++ b/.sqlx/query-0d80344b5e78f5c0d5de9cf752b860d3dd91b5749ccaa6a6e8cd65764d720905.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS count FROM thing_models WHERE model_uri = ?", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "0d80344b5e78f5c0d5de9cf752b860d3dd91b5749ccaa6a6e8cd65764d720905" +} diff --git a/.sqlx/query-0ed97286ed1a532f11a124fbb744081d97244d658f169b8e0bda21d40f4416dd.json b/.sqlx/query-0ed97286ed1a532f11a124fbb744081d97244d658f169b8e0bda21d40f4416dd.json new file mode 100644 index 0000000..075443e --- /dev/null +++ b/.sqlx/query-0ed97286ed1a532f11a124fbb744081d97244d658f169b8e0bda21d40f4416dd.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT cid, rkey FROM likes WHERE did = ? AND subject_uri = ?", + "describe": { + "columns": [ + { + "name": "cid", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "likes", + "name": "cid" + } + } + }, + { + "name": "rkey", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "likes", + "name": "rkey" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false + ] + }, + "hash": "0ed97286ed1a532f11a124fbb744081d97244d658f169b8e0bda21d40f4416dd" +} diff --git a/.sqlx/query-1133e593e6973e28ad03b0297f3bfd4c3f0918190f4ecd265314b213451bcf77.json b/.sqlx/query-1133e593e6973e28ad03b0297f3bfd4c3f0918190f4ecd265314b213451bcf77.json new file mode 100644 index 0000000..010781d --- /dev/null +++ b/.sqlx/query-1133e593e6973e28ad03b0297f3bfd4c3f0918190f4ecd265314b213451bcf77.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM things WHERE uri = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM things WHERE uri = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "1133e593e6973e28ad03b0297f3bfd4c3f0918190f4ecd265314b213451bcf77" +} diff --git a/.sqlx/query-2094180851ce8e023590116d2bf5cc3dd7b39e688652f305b957e61e8c18b661.json b/.sqlx/query-2094180851ce8e023590116d2bf5cc3dd7b39e688652f305b957e61e8c18b661.json new file mode 100644 index 0000000..2e46ddf --- /dev/null +++ b/.sqlx/query-2094180851ce8e023590116d2bf5cc3dd7b39e688652f305b957e61e8c18b661.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT file_json FROM upload_staging WHERE owner_did = ? AND upload_id = ? AND status = 'uploaded'", + "describe": { + "columns": [ + { + "name": "file_json", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "upload_staging", + "name": "file_json" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "2094180851ce8e023590116d2bf5cc3dd7b39e688652f305b957e61e8c18b661" +} diff --git a/.sqlx/query-4486bc1f0e41e6cf884dddaa52c24b6695604f173e09f05445add90eee55dae2.json b/.sqlx/query-4486bc1f0e41e6cf884dddaa52c24b6695604f173e09f05445add90eee55dae2.json new file mode 100644 index 0000000..8b09ecc --- /dev/null +++ b/.sqlx/query-4486bc1f0e41e6cf884dddaa52c24b6695604f173e09f05445add90eee55dae2.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM models WHERE did = ? AND uri = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM models WHERE did = ? AND uri = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "4486bc1f0e41e6cf884dddaa52c24b6695604f173e09f05445add90eee55dae2" +} diff --git a/.sqlx/query-563c3c23ec87f048b811625df4d623d1eb6cc6e95287f5c9c8e890fd727a056e.json b/.sqlx/query-563c3c23ec87f048b811625df4d623d1eb6cc6e95287f5c9c8e890fd727a056e.json new file mode 100644 index 0000000..943fdfe --- /dev/null +++ b/.sqlx/query-563c3c23ec87f048b811625df4d623d1eb6cc6e95287f5c9c8e890fd727a056e.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS count FROM model_parts WHERE part_uri = ?", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "563c3c23ec87f048b811625df4d623d1eb6cc6e95287f5c9c8e890fd727a056e" +} diff --git a/.sqlx/query-6b48422566a6ed1221ec83e82b8253653f0b2af89db76077142a71cc7f1643d0.json b/.sqlx/query-6b48422566a6ed1221ec83e82b8253653f0b2af89db76077142a71cc7f1643d0.json new file mode 100644 index 0000000..422f4bd --- /dev/null +++ b/.sqlx/query-6b48422566a6ed1221ec83e82b8253653f0b2af89db76077142a71cc7f1643d0.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM parts WHERE did = ? AND uri = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM parts WHERE did = ? AND uri = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "6b48422566a6ed1221ec83e82b8253653f0b2af89db76077142a71cc7f1643d0" +} diff --git a/.sqlx/query-7bdc04619643992941c43a742f045677fbe7a38b9ed034d70d5aea7be3603b9c.json b/.sqlx/query-7bdc04619643992941c43a742f045677fbe7a38b9ed034d70d5aea7be3603b9c.json new file mode 100644 index 0000000..8f35d8f --- /dev/null +++ b/.sqlx/query-7bdc04619643992941c43a742f045677fbe7a38b9ed034d70d5aea7be3603b9c.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT p.did, p.rkey, p.uri FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri JOIN parts p ON p.uri = mp.part_uri WHERE tm.thing_uri = ? ORDER BY tm.position, mp.position", + "describe": { + "columns": [ + { + "name": "did", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "parts", + "name": "did" + } + } + }, + { + "name": "rkey", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "parts", + "name": "rkey" + } + } + }, + { + "name": "uri", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "parts", + "name": "uri" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "7bdc04619643992941c43a742f045677fbe7a38b9ed034d70d5aea7be3603b9c" +} diff --git a/.sqlx/query-8b02bdc1204d6e4afd30d29aa605e34777a5ab609d8eb454836d676800ff0511.json b/.sqlx/query-8b02bdc1204d6e4afd30d29aa605e34777a5ab609d8eb454836d676800ff0511.json deleted file mode 100644 index fec5bcc..0000000 --- a/.sqlx/query-8b02bdc1204d6e4afd30d29aa605e34777a5ab609d8eb454836d676800ff0511.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO tags (did, rkey, subject_uri, tag, created_at) VALUES (?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "8b02bdc1204d6e4afd30d29aa605e34777a5ab609d8eb454836d676800ff0511" -} diff --git a/.sqlx/query-993cd27061ba88a858f7021f8e86d116f2dec107de7fdba8cc8644ebbc6d0ae3.json b/.sqlx/query-993cd27061ba88a858f7021f8e86d116f2dec107de7fdba8cc8644ebbc6d0ae3.json new file mode 100644 index 0000000..30ee6b6 --- /dev/null +++ b/.sqlx/query-993cd27061ba88a858f7021f8e86d116f2dec107de7fdba8cc8644ebbc6d0ae3.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT cid, rkey FROM saves WHERE did = ? AND subject_uri = ?", + "describe": { + "columns": [ + { + "name": "cid", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "saves", + "name": "cid" + } + } + }, + { + "name": "rkey", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "saves", + "name": "rkey" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false + ] + }, + "hash": "993cd27061ba88a858f7021f8e86d116f2dec107de7fdba8cc8644ebbc6d0ae3" +} diff --git a/.sqlx/query-af25cf6fdd1ddc048069a952e8a25dad4abd7cba9b604fe2a25d77bcf3d5f1af.json b/.sqlx/query-af25cf6fdd1ddc048069a952e8a25dad4abd7cba9b604fe2a25d77bcf3d5f1af.json deleted file mode 100644 index 8e08469..0000000 --- a/.sqlx/query-af25cf6fdd1ddc048069a952e8a25dad4abd7cba9b604fe2a25d77bcf3d5f1af.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO listitems (did, rkey, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "af25cf6fdd1ddc048069a952e8a25dad4abd7cba9b604fe2a25d77bcf3d5f1af" -} diff --git a/.sqlx/query-b4a4296cbfee1f54b3e025416499fbeb50c24b15248ac8516ede592e68b09523.json b/.sqlx/query-b4a4296cbfee1f54b3e025416499fbeb50c24b15248ac8516ede592e68b09523.json deleted file mode 100644 index 377c421..0000000 --- a/.sqlx/query-b4a4296cbfee1f54b3e025416499fbeb50c24b15248ac8516ede592e68b09523.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO saves (did, rkey, subject_uri, note, created_at) VALUES (?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "b4a4296cbfee1f54b3e025416499fbeb50c24b15248ac8516ede592e68b09523" -} diff --git a/.sqlx/query-bc2637c2b2950f580859536b190e13b7f1192296f332354b9e174150fd5e89ac.json b/.sqlx/query-bc2637c2b2950f580859536b190e13b7f1192296f332354b9e174150fd5e89ac.json new file mode 100644 index 0000000..242c9f1 --- /dev/null +++ b/.sqlx/query-bc2637c2b2950f580859536b190e13b7f1192296f332354b9e174150fd5e89ac.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT did, rkey, uri FROM things WHERE uri = ?", + "describe": { + "columns": [ + { + "name": "did", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "things", + "name": "did" + } + } + }, + { + "name": "rkey", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "things", + "name": "rkey" + } + } + }, + { + "name": "uri", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "things", + "name": "uri" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "bc2637c2b2950f580859536b190e13b7f1192296f332354b9e174150fd5e89ac" +} diff --git a/.sqlx/query-c9ef48617a88d2241b9959825184b59d1c25cf1024571297cbe32c3efb86c03d.json b/.sqlx/query-c9ef48617a88d2241b9959825184b59d1c25cf1024571297cbe32c3efb86c03d.json deleted file mode 100644 index 15d150b..0000000 --- a/.sqlx/query-c9ef48617a88d2241b9959825184b59d1c25cf1024571297cbe32c3efb86c03d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO likes (did, rkey, subject_uri, created_at) VALUES (?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "c9ef48617a88d2241b9959825184b59d1c25cf1024571297cbe32c3efb86c03d" -} diff --git a/.sqlx/query-cb4c8e4f4748a49049c4e942e6c1e6e1f9ae120827a7abf0d150f1a37936fd4f.json b/.sqlx/query-cb4c8e4f4748a49049c4e942e6c1e6e1f9ae120827a7abf0d150f1a37936fd4f.json new file mode 100644 index 0000000..51a0c74 --- /dev/null +++ b/.sqlx/query-cb4c8e4f4748a49049c4e942e6c1e6e1f9ae120827a7abf0d150f1a37936fd4f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO saves (did, rkey, cid, subject_uri, note, created_at) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "cb4c8e4f4748a49049c4e942e6c1e6e1f9ae120827a7abf0d150f1a37936fd4f" +} diff --git a/.sqlx/query-cd961657cb3d0a41d2e6b503d4038849f8350117fb65f773f9429709dd588a09.json b/.sqlx/query-cd961657cb3d0a41d2e6b503d4038849f8350117fb65f773f9429709dd588a09.json new file mode 100644 index 0000000..e7e0d96 --- /dev/null +++ b/.sqlx/query-cd961657cb3d0a41d2e6b503d4038849f8350117fb65f773f9429709dd588a09.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO tags (did, rkey, cid, subject_uri, tag, created_at) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "cd961657cb3d0a41d2e6b503d4038849f8350117fb65f773f9429709dd588a09" +} diff --git a/.sqlx/query-ce65448db03ba94432bb911c797b33277438ac7fd538b9e03334988dbd014fac.json b/.sqlx/query-ce65448db03ba94432bb911c797b33277438ac7fd538b9e03334988dbd014fac.json new file mode 100644 index 0000000..6437466 --- /dev/null +++ b/.sqlx/query-ce65448db03ba94432bb911c797b33277438ac7fd538b9e03334988dbd014fac.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM parts WHERE uri = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM parts WHERE uri = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "ce65448db03ba94432bb911c797b33277438ac7fd538b9e03334988dbd014fac" +} diff --git a/.sqlx/query-db731da68101b9edd9662d51f7fce22044ab9f955f59b1d57ca2a03837aa0728.json b/.sqlx/query-db731da68101b9edd9662d51f7fce22044ab9f955f59b1d57ca2a03837aa0728.json new file mode 100644 index 0000000..29f159e --- /dev/null +++ b/.sqlx/query-db731da68101b9edd9662d51f7fce22044ab9f955f59b1d57ca2a03837aa0728.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO listitems (did, rkey, cid, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "db731da68101b9edd9662d51f7fce22044ab9f955f59b1d57ca2a03837aa0728" +} diff --git a/.sqlx/query-e16314e2226f9cf2334ca747403b693fb60120cfed020b300efe1f55e1e9564c.json b/.sqlx/query-e16314e2226f9cf2334ca747403b693fb60120cfed020b300efe1f55e1e9564c.json new file mode 100644 index 0000000..abca9a3 --- /dev/null +++ b/.sqlx/query-e16314e2226f9cf2334ca747403b693fb60120cfed020b300efe1f55e1e9564c.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT m.did, m.rkey, m.uri FROM thing_models tm JOIN models m ON m.uri = tm.model_uri WHERE tm.thing_uri = ? ORDER BY tm.position", + "describe": { + "columns": [ + { + "name": "did", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "models", + "name": "did" + } + } + }, + { + "name": "rkey", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "models", + "name": "rkey" + } + } + }, + { + "name": "uri", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "models", + "name": "uri" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "e16314e2226f9cf2334ca747403b693fb60120cfed020b300efe1f55e1e9564c" +} diff --git a/.sqlx/query-e9573839c7d4c31f3960e6415774d5f65ae2e0b6ac708f00bb750974412c4fb2.json b/.sqlx/query-e9573839c7d4c31f3960e6415774d5f65ae2e0b6ac708f00bb750974412c4fb2.json new file mode 100644 index 0000000..8340049 --- /dev/null +++ b/.sqlx/query-e9573839c7d4c31f3960e6415774d5f65ae2e0b6ac708f00bb750974412c4fb2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO likes (did, rkey, cid, subject_uri, created_at) VALUES (?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "e9573839c7d4c31f3960e6415774d5f65ae2e0b6ac708f00bb750974412c4fb2" +} diff --git a/.sqlx/query-f727bf890c756a13373922ed6f5f948f77dd5faa65c0b75664142a4b888986da.json b/.sqlx/query-f727bf890c756a13373922ed6f5f948f77dd5faa65c0b75664142a4b888986da.json new file mode 100644 index 0000000..23e404d --- /dev/null +++ b/.sqlx/query-f727bf890c756a13373922ed6f5f948f77dd5faa65c0b75664142a4b888986da.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO upload_staging (owner_did, upload_id, sha256, mime_type, size, filename, status, file_json, chunks_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'uploaded', ?, ?, ?, ?) ON CONFLICT(owner_did, upload_id) DO UPDATE SET sha256 = excluded.sha256, mime_type = excluded.mime_type, size = excluded.size, filename = excluded.filename, status = excluded.status, file_json = excluded.file_json, chunks_json = excluded.chunks_json, updated_at = excluded.updated_at", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "f727bf890c756a13373922ed6f5f948f77dd5faa65c0b75664142a4b888986da" +} diff --git a/Cargo.lock b/Cargo.lock index 23b608a..3bbb2b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc-swap" @@ -1572,6 +1575,17 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -6184,6 +6198,7 @@ dependencies = [ "dotenvy", "futures", "http", + "http-body-util", "hydrant", "jacquard", "jacquard-axum", @@ -6197,6 +6212,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "sha2 0.10.9", "sqlx", "stl_io", "thiserror 2.0.18", @@ -6211,6 +6227,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "zip", ] [[package]] @@ -9886,6 +9903,21 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "indexmap", + "memchr", + "thiserror 2.0.18", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 77a0468..9267390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ server = [ "dep:anyhow", "dep:dotenvy", "dep:chrono", + "dep:http-body-util", "dep:tracing-subscriber", "dep:rustls", # OAuth (PM-27): confidential client, private cookie jar key, secret sources. @@ -49,6 +50,8 @@ server = [ "dep:keyring", "dep:base64", + "dep:sha2", + "dep:zip", ] [dependencies] dioxus = { version = "0.7", features = ["router", "fullstack"] } @@ -65,7 +68,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } three-d-asset = { version = "0.10", default-features = false } stl_io = "0.11" -axum = { version = "0.8", optional = true } +axum = { version = "0.8", optional = true, features = ["multipart"] } tower = { version = "0.5", optional = true } tower-http = { version = "0.6", optional = true, features = ["trace"] } three-d = { version = "0.19.0", default-features = false } @@ -97,6 +100,7 @@ tracing-subscriber = { version = "0.3", optional = true, default-features = fals "env-filter", ] } http = "1.4" +http-body-util = { version = "0.1", optional = true } reqwest = { version = "0.12", default-features = false, features = ["json", "charset"] } # PM-27 OAuth: private cookie-jar signing key and swappable secret sources @@ -105,6 +109,8 @@ axum-extra = { version = "0.10", optional = true, features = ["cookie"] } keyring = { version = "3", optional = true } base64 = { version = "0.22", optional = true } +sha2 = { version = "0.10", optional = true } +zip = { version = "2", default-features = false, optional = true } [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] @@ -118,12 +124,17 @@ web-sys = { version = "0.3", features = ["Window", "Document", "Element", "Event [dev-dependencies] anyhow = "1" axum = "0.8" +axum-extra = { version = "0.10", features = ["cookie"] } +base64 = "0.22" chrono = "0.4" futures = "0.3" hydrant = { git = "https://tangled.org/ptr.pet/hydrant", rev = "1145fa1bb194372ceea81ab48b5d68d304da34ad", default-features = false, features = ["indexer", "indexer_stream"] } +http-body-util = "0.1" jacquard-axum = { workspace = true } +keyring = "3" libsqlite3-sys = { version = "0.37", features = ["bundled"] } rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std"] } +sha2 = "0.10" sqlx = { version = "0.9", default-features = false, features = [ "runtime-tokio", "sqlite", diff --git a/crates/polymodel-api/lexicons/space_polymodel_actor_bootstrapProfile.json b/crates/polymodel-api/lexicons/space_polymodel_actor_bootstrapProfile.json new file mode 100644 index 0000000..810da6e --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_actor_bootstrapProfile.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "space.polymodel.actor.bootstrapProfile", + "defs": { + "main": { + "type": "procedure", + "description": "One-time authenticated bootstrap of the actor's Polymodel profile from their own PDS profile records. Creates space.polymodel.actor.profile/self only when absent locally and PDS-side.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "status", + "profile" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "profile": { + "type": "ref", + "ref": "space.polymodel.actor.defs#profileView" + }, + "status": { + "type": "string", + "knownValues": [ + "existingLocal", + "existingRemote", + "created" + ] + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_createLike.json b/crates/polymodel-api/lexicons/space_polymodel_graph_createLike.json new file mode 100644 index 0000000..bf73f82 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_createLike.json @@ -0,0 +1,48 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createLike", + "defs": { + "main": { + "type": "procedure", + "description": "Create a like record for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "created": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_createSave.json b/crates/polymodel-api/lexicons/space_polymodel_graph_createSave.json new file mode 100644 index 0000000..2fbffdd --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_createSave.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createSave", + "defs": { + "main": { + "type": "procedure", + "description": "Create a save record for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "note": { + "type": "string", + "maxLength": 2000 + }, + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "created": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_createTag.json b/crates/polymodel-api/lexicons/space_polymodel_graph_createTag.json new file mode 100644 index 0000000..abcec05 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_createTag.json @@ -0,0 +1,54 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createTag", + "defs": { + "main": { + "type": "procedure", + "description": "Create a tag assertion for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject", + "tag" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "tag": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "created": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_deleteLike.json b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteLike.json new file mode 100644 index 0000000..5cbcf4c --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteLike.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteLike", + "defs": { + "main": { + "type": "procedure", + "description": "Delete the authenticated actor's like by subject or like record URI, then eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_deleteSave.json b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteSave.json new file mode 100644 index 0000000..fb8a11e --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteSave.json @@ -0,0 +1,44 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteSave", + "defs": { + "main": { + "type": "procedure", + "description": "Delete the authenticated actor's save by subject or save record URI, then eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_graph_deleteTag.json b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteTag.json new file mode 100644 index 0000000..fcaf0d8 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_graph_deleteTag.json @@ -0,0 +1,43 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteTag", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a specific tag assertion record for the authenticated actor, then eagerly project viewer/count state. Tag delete requires a concrete tag record URI because tags are rkey-idempotent, not unique by subject+tag.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_library_deleteThing.json b/crates/polymodel-api/lexicons/space_polymodel_library_deleteThing.json new file mode 100644 index 0000000..242a873 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_library_deleteThing.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.deleteThing", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an owned thing and its owned model/part composition records from the authenticated actor's PDS, then eagerly remove local projections.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "deleted" + ], + "properties": { + "deleted": { + "type": "boolean" + }, + "deletedUris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_library_publishThing.json b/crates/polymodel-api/lexicons/space_polymodel_library_publishThing.json new file mode 100644 index 0000000..d7aaef2 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_library_publishThing.json @@ -0,0 +1,296 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.publishThing", + "defs": { + "main": { + "type": "procedure", + "description": "Publish a thing by creating part records first, model records second, and the thing record last on the authenticated actor's PDS, then eagerly projecting every successful record locally.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "thing" + ], + "properties": { + "thing": { + "type": "ref", + "ref": "#thingInput" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "thing", + "models" + ], + "properties": { + "models": { + "type": "array", + "items": { + "type": "ref", + "ref": "#modelPublishResult" + } + }, + "thing": { + "type": "ref", + "ref": "#recordRef" + } + } + } + } + }, + "modelInput": { + "type": "object", + "required": [ + "name", + "parts" + ], + "properties": { + "cover": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 8 + }, + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 100 + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "license": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "parts": { + "type": "array", + "items": { + "type": "ref", + "ref": "#partInput" + }, + "maxLength": 1024 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "summary": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 16 + } + } + }, + "modelPublishResult": { + "type": "object", + "required": [ + "model", + "parts" + ], + "properties": { + "model": { + "type": "ref", + "ref": "#recordRef" + }, + "parts": { + "type": "array", + "items": { + "type": "ref", + "ref": "#recordRef" + } + } + } + }, + "partInput": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "dimensions": { + "type": "ref", + "ref": "space.polymodel.library.defs#bbox" + }, + "file": { + "type": "ref", + "ref": "space.polymodel.library.defs#file" + }, + "format": { + "type": "string" + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "notes": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "printSettings": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 50 + }, + "units": { + "type": "string" + }, + "uploadId": { + "type": "string" + } + } + }, + "recordRef": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "thingInput": { + "type": "object", + "required": [ + "name", + "license", + "models" + ], + "properties": { + "cover": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 8 + }, + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 100 + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "license": { + "type": "string" + }, + "models": { + "type": "array", + "items": { + "type": "ref", + "ref": "#modelInput" + }, + "maxLength": 256 + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "summary": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 16 + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_library_stageFile.json b/crates/polymodel-api/lexicons/space_polymodel_library_stageFile.json new file mode 100644 index 0000000..fb0be3a --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_library_stageFile.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.stageFile", + "defs": { + "main": { + "type": "procedure", + "description": "Stage one raw geometry payload for publishing. The HTTP body is raw bytes, not base64 JSON; metadata is provided by headers. The server chunks and uploads blobs through the authenticated actor's PDS, stores owner-scoped staging metadata, and returns defs#file-compatible metadata.", + "input": { + "encoding": "*/*" + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uploadId", + "file" + ], + "properties": { + "file": { + "type": "ref", + "ref": "space.polymodel.library.defs#file" + }, + "filename": { + "type": "string" + }, + "status": { + "type": "string", + "knownValues": [ + "uploaded" + ] + }, + "uploadId": { + "type": "string" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/lexicons/space_polymodel_library_updateThing.json b/crates/polymodel-api/lexicons/space_polymodel_library_updateThing.json new file mode 100644 index 0000000..05dc794 --- /dev/null +++ b/crates/polymodel-api/lexicons/space_polymodel_library_updateThing.json @@ -0,0 +1,272 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.updateThing", + "defs": { + "main": { + "type": "procedure", + "description": "Update an owned thing composition with updated thing/model/part data. Existing model/part inputs with uri are updated in place; inputs without uri create new records. Removed child records are detached from the updated parent records but not deleted, so they can be restored later.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "thing" + ], + "properties": { + "thing": { + "type": "ref", + "ref": "#thingInput" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "thing", + "models" + ], + "properties": { + "models": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.publishThing#modelPublishResult" + } + }, + "thing": { + "type": "ref", + "ref": "space.polymodel.library.publishThing#recordRef" + } + } + } + } + }, + "modelInput": { + "type": "object", + "required": [ + "name", + "parts" + ], + "properties": { + "cover": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 8 + }, + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 100 + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "license": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "parts": { + "type": "array", + "items": { + "type": "ref", + "ref": "#partInput" + }, + "maxLength": 1024 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "summary": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 16 + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "partInput": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "dimensions": { + "type": "ref", + "ref": "space.polymodel.library.defs#bbox" + }, + "file": { + "type": "ref", + "ref": "space.polymodel.library.defs#file" + }, + "format": { + "type": "string" + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "notes": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "printSettings": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 50 + }, + "units": { + "type": "string" + }, + "uploadId": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "thingInput": { + "type": "object", + "required": [ + "name", + "license", + "models" + ], + "properties": { + "cover": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 8 + }, + "derivedFrom": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "instructions": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 100 + }, + "intendedToWorkWith": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + }, + "maxLength": 32 + }, + "license": { + "type": "string" + }, + "models": { + "type": "array", + "items": { + "type": "ref", + "ref": "#modelInput" + }, + "maxLength": 256 + }, + "name": { + "type": "string", + "maxLength": 256, + "maxGraphemes": 64 + }, + "previews": { + "type": "array", + "items": { + "type": "ref", + "ref": "space.polymodel.library.defs#image" + }, + "maxLength": 12 + }, + "summary": { + "type": "string", + "maxLength": 2000, + "maxGraphemes": 300 + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "maxGraphemes": 32 + }, + "maxLength": 16 + } + } + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/actor.rs b/crates/polymodel-api/src/space_polymodel/actor.rs index 86d352f..06ab798 100644 --- a/crates/polymodel-api/src/space_polymodel/actor.rs +++ b/crates/polymodel-api/src/space_polymodel/actor.rs @@ -6,6 +6,7 @@ // Any manual changes will be overwritten on the next regeneration. //! Generated bindings for the `space.polymodel.actor` Lexicon namespace/module. +pub mod bootstrap_profile; pub mod get_profile; pub mod get_session; pub mod profile; diff --git a/crates/polymodel-api/src/space_polymodel/actor/bootstrap_profile.rs b/crates/polymodel-api/src/space_polymodel/actor/bootstrap_profile.rs new file mode 100644 index 0000000..10ea38f --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/actor/bootstrap_profile.rs @@ -0,0 +1,161 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.actor.bootstrapProfile +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::{AtUri, Cid}; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::space_polymodel::actor::ProfileView; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct BootstrapProfileOutput { + #[serde(skip_serializing_if = "Option::is_none")] + pub cid: Option>, + pub profile: ProfileView, + pub status: BootstrapProfileOutputStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum BootstrapProfileOutputStatus { + ExistingLocal, + ExistingRemote, + Created, + Other(S), +} + +impl BootstrapProfileOutputStatus { + pub fn as_str(&self) -> &str { + match self { + Self::ExistingLocal => "existingLocal", + Self::ExistingRemote => "existingRemote", + Self::Created => "created", + Self::Other(s) => s.as_ref(), + } + } + /// Construct from a string-like value, matching known values. + pub fn from_value(s: S) -> Self { + match s.as_ref() { + "existingLocal" => Self::ExistingLocal, + "existingRemote" => Self::ExistingRemote, + "created" => Self::Created, + _ => Self::Other(s), + } + } +} + +impl core::fmt::Display for BootstrapProfileOutputStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl AsRef for BootstrapProfileOutputStatus { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Serialize for BootstrapProfileOutputStatus { + fn serialize(&self, serializer: Ser) -> Result + where + Ser: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, S: Deserialize<'de> + BosStr> Deserialize<'de> +for BootstrapProfileOutputStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = S::deserialize(deserializer)?; + Ok(Self::from_value(s)) + } +} + +impl Default for BootstrapProfileOutputStatus { + fn default() -> Self { + Self::Other(Default::default()) + } +} + +impl jacquard_common::IntoStatic for BootstrapProfileOutputStatus +where + S: BosStr + jacquard_common::IntoStatic, + S::Output: BosStr, +{ + type Output = BootstrapProfileOutputStatus; + fn into_static(self) -> Self::Output { + match self { + BootstrapProfileOutputStatus::ExistingLocal => { + BootstrapProfileOutputStatus::ExistingLocal + } + BootstrapProfileOutputStatus::ExistingRemote => { + BootstrapProfileOutputStatus::ExistingRemote + } + BootstrapProfileOutputStatus::Created => { + BootstrapProfileOutputStatus::Created + } + BootstrapProfileOutputStatus::Other(v) => { + BootstrapProfileOutputStatus::Other(v.into_static()) + } + } + } +} + +/** Request marker for the `space.polymodel.actor.bootstrapProfile` procedure. + +This endpoint has no request parameters or input body; send this marker with `jacquard::Client`.*/ + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Copy)] +pub struct BootstrapProfile; +/** Response marker for the `space.polymodel.actor.bootstrapProfile` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `BootstrapProfileOutput` for this endpoint.*/ +pub struct BootstrapProfileResponse; +impl jacquard_common::xrpc::XrpcResp for BootstrapProfileResponse { + const NSID: &'static str = "space.polymodel.actor.bootstrapProfile"; + const ENCODING: &'static str = "application/json"; + type Output = BootstrapProfileOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for BootstrapProfile { + const NSID: &'static str = "space.polymodel.actor.bootstrapProfile"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = BootstrapProfileResponse; +} + +/** Endpoint marker for the `space.polymodel.actor.bootstrapProfile` procedure. + +Path: `/xrpc/space.polymodel.actor.bootstrapProfile`. The request payload type is `BootstrapProfile`; use this marker with lower-level `XrpcEndpoint` APIs when you need endpoint metadata.*/ +pub struct BootstrapProfileRequest; +impl jacquard_common::xrpc::XrpcEndpoint for BootstrapProfileRequest { + const PATH: &'static str = "/xrpc/space.polymodel.actor.bootstrapProfile"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = BootstrapProfile; + type Response = BootstrapProfileResponse; +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph.rs b/crates/polymodel-api/src/space_polymodel/graph.rs index e6e01e3..5099554 100644 --- a/crates/polymodel-api/src/space_polymodel/graph.rs +++ b/crates/polymodel-api/src/space_polymodel/graph.rs @@ -4,6 +4,12 @@ // Any manual changes will be overwritten on the next regeneration. //! Generated bindings for the `space.polymodel.graph` Lexicon namespace/module. +pub mod create_like; +pub mod create_save; +pub mod create_tag; +pub mod delete_like; +pub mod delete_save; +pub mod delete_tag; pub mod get_list; pub mod like; pub mod list; diff --git a/crates/polymodel-api/src/space_polymodel/graph/create_like.rs b/crates/polymodel-api/src/space_polymodel/graph/create_like.rs new file mode 100644 index 0000000..b01a7b1 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/create_like.rs @@ -0,0 +1,189 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.createLike +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::{AtUri, Cid}; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateLike { + pub subject: StrongRef, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateLikeOutput { + pub cid: Cid, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.createLike` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `CreateLikeOutput` for this endpoint.*/ +pub struct CreateLikeResponse; +impl jacquard_common::xrpc::XrpcResp for CreateLikeResponse { + const NSID: &'static str = "space.polymodel.graph.createLike"; + const ENCODING: &'static str = "application/json"; + type Output = CreateLikeOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for CreateLike { + const NSID: &'static str = "space.polymodel.graph.createLike"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = CreateLikeResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.createLike` procedure. + +Path: `/xrpc/space.polymodel.graph.createLike`. The request payload type is `CreateLike`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct CreateLikeRequest; +impl jacquard_common::xrpc::XrpcEndpoint for CreateLikeRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.createLike"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = CreateLike; + type Response = CreateLikeResponse; +} + +pub mod create_like_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Subject; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData St>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct CreateLikeBuilder { + _state: PhantomData St>, + _fields: (Option>,), + _type: PhantomData S>, +} + +impl CreateLike { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> CreateLikeBuilder { + CreateLikeBuilder::new() + } +} + +impl CreateLike { + /// Create a new builder for this type + pub fn builder() -> CreateLikeBuilder { + CreateLikeBuilder::builder() + } +} + +impl CreateLikeBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + CreateLikeBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl CreateLikeBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + CreateLikeBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl CreateLikeBuilder +where + St: create_like_state::State, + St::Subject: create_like_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> CreateLikeBuilder, S> { + self._fields.0 = Option::Some(value.into()); + CreateLikeBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl CreateLikeBuilder +where + St: create_like_state::State, + St::Subject: create_like_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> CreateLike { + CreateLike { + subject: self._fields.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> CreateLike { + CreateLike { + subject: self._fields.0.unwrap(), + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph/create_save.rs b/crates/polymodel-api/src/space_polymodel/graph/create_save.rs new file mode 100644 index 0000000..3fc7a00 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/create_save.rs @@ -0,0 +1,206 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.createSave +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::{AtUri, Cid}; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateSave { + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option, + pub subject: StrongRef, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateSaveOutput { + pub cid: Cid, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.createSave` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `CreateSaveOutput` for this endpoint.*/ +pub struct CreateSaveResponse; +impl jacquard_common::xrpc::XrpcResp for CreateSaveResponse { + const NSID: &'static str = "space.polymodel.graph.createSave"; + const ENCODING: &'static str = "application/json"; + type Output = CreateSaveOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for CreateSave { + const NSID: &'static str = "space.polymodel.graph.createSave"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = CreateSaveResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.createSave` procedure. + +Path: `/xrpc/space.polymodel.graph.createSave`. The request payload type is `CreateSave`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct CreateSaveRequest; +impl jacquard_common::xrpc::XrpcEndpoint for CreateSaveRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.createSave"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = CreateSave; + type Response = CreateSaveResponse; +} + +pub mod create_save_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Subject; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData St>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct CreateSaveBuilder { + _state: PhantomData St>, + _fields: (Option, Option>), + _type: PhantomData S>, +} + +impl CreateSave { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> CreateSaveBuilder { + CreateSaveBuilder::new() + } +} + +impl CreateSave { + /// Create a new builder for this type + pub fn builder() -> CreateSaveBuilder { + CreateSaveBuilder::builder() + } +} + +impl CreateSaveBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + CreateSaveBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl CreateSaveBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + CreateSaveBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl CreateSaveBuilder { + /// Set the `note` field (optional) + pub fn note(mut self, value: impl Into>) -> Self { + self._fields.0 = value.into(); + self + } + /// Set the `note` field to an Option value (optional) + pub fn maybe_note(mut self, value: Option) -> Self { + self._fields.0 = value; + self + } +} + +impl CreateSaveBuilder +where + St: create_save_state::State, + St::Subject: create_save_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> CreateSaveBuilder, S> { + self._fields.1 = Option::Some(value.into()); + CreateSaveBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl CreateSaveBuilder +where + St: create_save_state::State, + St::Subject: create_save_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> CreateSave { + CreateSave { + note: self._fields.0, + subject: self._fields.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> CreateSave { + CreateSave { + note: self._fields.0, + subject: self._fields.1.unwrap(), + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph/create_tag.rs b/crates/polymodel-api/src/space_polymodel/graph/create_tag.rs new file mode 100644 index 0000000..910aac5 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/create_tag.rs @@ -0,0 +1,224 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.createTag +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::{AtUri, Cid}; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateTag { + pub subject: StrongRef, + pub tag: S, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct CreateTagOutput { + pub cid: Cid, + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option, + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.createTag` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `CreateTagOutput` for this endpoint.*/ +pub struct CreateTagResponse; +impl jacquard_common::xrpc::XrpcResp for CreateTagResponse { + const NSID: &'static str = "space.polymodel.graph.createTag"; + const ENCODING: &'static str = "application/json"; + type Output = CreateTagOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for CreateTag { + const NSID: &'static str = "space.polymodel.graph.createTag"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = CreateTagResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.createTag` procedure. + +Path: `/xrpc/space.polymodel.graph.createTag`. The request payload type is `CreateTag`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct CreateTagRequest; +impl jacquard_common::xrpc::XrpcEndpoint for CreateTagRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.createTag"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = CreateTag; + type Response = CreateTagResponse; +} + +pub mod create_tag_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Subject; + type Tag; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Subject = Unset; + type Tag = Unset; + } + ///State transition - sets the `subject` field to Set + pub struct SetSubject(PhantomData St>); + impl sealed::Sealed for SetSubject {} + impl State for SetSubject { + type Subject = Set; + type Tag = St::Tag; + } + ///State transition - sets the `tag` field to Set + pub struct SetTag(PhantomData St>); + impl sealed::Sealed for SetTag {} + impl State for SetTag { + type Subject = St::Subject; + type Tag = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `subject` field + pub struct subject(()); + ///Marker type for the `tag` field + pub struct tag(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct CreateTagBuilder { + _state: PhantomData St>, + _fields: (Option>, Option), + _type: PhantomData S>, +} + +impl CreateTag { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> CreateTagBuilder { + CreateTagBuilder::new() + } +} + +impl CreateTag { + /// Create a new builder for this type + pub fn builder() -> CreateTagBuilder { + CreateTagBuilder::builder() + } +} + +impl CreateTagBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + CreateTagBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl CreateTagBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + CreateTagBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl CreateTagBuilder +where + St: create_tag_state::State, + St::Subject: create_tag_state::IsUnset, +{ + /// Set the `subject` field (required) + pub fn subject( + mut self, + value: impl Into>, + ) -> CreateTagBuilder, S> { + self._fields.0 = Option::Some(value.into()); + CreateTagBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl CreateTagBuilder +where + St: create_tag_state::State, + St::Tag: create_tag_state::IsUnset, +{ + /// Set the `tag` field (required) + pub fn tag( + mut self, + value: impl Into, + ) -> CreateTagBuilder, S> { + self._fields.1 = Option::Some(value.into()); + CreateTagBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl CreateTagBuilder +where + St: create_tag_state::State, + St::Subject: create_tag_state::IsSet, + St::Tag: create_tag_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> CreateTag { + CreateTag { + subject: self._fields.0.unwrap(), + tag: self._fields.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> CreateTag { + CreateTag { + subject: self._fields.0.unwrap(), + tag: self._fields.1.unwrap(), + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph/delete_like.rs b/crates/polymodel-api/src/space_polymodel/graph/delete_like.rs new file mode 100644 index 0000000..dd33aec --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/delete_like.rs @@ -0,0 +1,73 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.deleteLike +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::AtUri; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteLike { + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteLikeOutput { + pub deleted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.deleteLike` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `DeleteLikeOutput` for this endpoint.*/ +pub struct DeleteLikeResponse; +impl jacquard_common::xrpc::XrpcResp for DeleteLikeResponse { + const NSID: &'static str = "space.polymodel.graph.deleteLike"; + const ENCODING: &'static str = "application/json"; + type Output = DeleteLikeOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for DeleteLike { + const NSID: &'static str = "space.polymodel.graph.deleteLike"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = DeleteLikeResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.deleteLike` procedure. + +Path: `/xrpc/space.polymodel.graph.deleteLike`. The request payload type is `DeleteLike`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct DeleteLikeRequest; +impl jacquard_common::xrpc::XrpcEndpoint for DeleteLikeRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.deleteLike"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = DeleteLike; + type Response = DeleteLikeResponse; +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph/delete_save.rs b/crates/polymodel-api/src/space_polymodel/graph/delete_save.rs new file mode 100644 index 0000000..001dd4a --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/delete_save.rs @@ -0,0 +1,73 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.deleteSave +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::AtUri; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteSave { + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteSaveOutput { + pub deleted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.deleteSave` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `DeleteSaveOutput` for this endpoint.*/ +pub struct DeleteSaveResponse; +impl jacquard_common::xrpc::XrpcResp for DeleteSaveResponse { + const NSID: &'static str = "space.polymodel.graph.deleteSave"; + const ENCODING: &'static str = "application/json"; + type Output = DeleteSaveOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for DeleteSave { + const NSID: &'static str = "space.polymodel.graph.deleteSave"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = DeleteSaveResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.deleteSave` procedure. + +Path: `/xrpc/space.polymodel.graph.deleteSave`. The request payload type is `DeleteSave`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct DeleteSaveRequest; +impl jacquard_common::xrpc::XrpcEndpoint for DeleteSaveRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.deleteSave"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = DeleteSave; + type Response = DeleteSaveResponse; +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/graph/delete_tag.rs b/crates/polymodel-api/src/space_polymodel/graph/delete_tag.rs new file mode 100644 index 0000000..0b5c777 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/graph/delete_tag.rs @@ -0,0 +1,187 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.graph.deleteTag +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::AtUri; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteTag { + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteTagOutput { + pub deleted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.graph.deleteTag` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `DeleteTagOutput` for this endpoint.*/ +pub struct DeleteTagResponse; +impl jacquard_common::xrpc::XrpcResp for DeleteTagResponse { + const NSID: &'static str = "space.polymodel.graph.deleteTag"; + const ENCODING: &'static str = "application/json"; + type Output = DeleteTagOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for DeleteTag { + const NSID: &'static str = "space.polymodel.graph.deleteTag"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = DeleteTagResponse; +} + +/** Endpoint marker for the `space.polymodel.graph.deleteTag` procedure. + +Path: `/xrpc/space.polymodel.graph.deleteTag`. The request payload type is `DeleteTag`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct DeleteTagRequest; +impl jacquard_common::xrpc::XrpcEndpoint for DeleteTagRequest { + const PATH: &'static str = "/xrpc/space.polymodel.graph.deleteTag"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = DeleteTag; + type Response = DeleteTagResponse; +} + +pub mod delete_tag_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Uri; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Uri = Unset; + } + ///State transition - sets the `uri` field to Set + pub struct SetUri(PhantomData St>); + impl sealed::Sealed for SetUri {} + impl State for SetUri { + type Uri = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `uri` field + pub struct uri(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct DeleteTagBuilder { + _state: PhantomData St>, + _fields: (Option>,), + _type: PhantomData S>, +} + +impl DeleteTag { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> DeleteTagBuilder { + DeleteTagBuilder::new() + } +} + +impl DeleteTag { + /// Create a new builder for this type + pub fn builder() -> DeleteTagBuilder { + DeleteTagBuilder::builder() + } +} + +impl DeleteTagBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + DeleteTagBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl DeleteTagBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + DeleteTagBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl DeleteTagBuilder +where + St: delete_tag_state::State, + St::Uri: delete_tag_state::IsUnset, +{ + /// Set the `uri` field (required) + pub fn uri( + mut self, + value: impl Into>, + ) -> DeleteTagBuilder, S> { + self._fields.0 = Option::Some(value.into()); + DeleteTagBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl DeleteTagBuilder +where + St: delete_tag_state::State, + St::Uri: delete_tag_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> DeleteTag { + DeleteTag { + uri: self._fields.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> DeleteTag { + DeleteTag { + uri: self._fields.0.unwrap(), + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/library.rs b/crates/polymodel-api/src/space_polymodel/library.rs index 697398e..d741a14 100644 --- a/crates/polymodel-api/src/space_polymodel/library.rs +++ b/crates/polymodel-api/src/space_polymodel/library.rs @@ -6,14 +6,18 @@ // Any manual changes will be overwritten on the next regeneration. //! Generated bindings for the `space.polymodel.library` Lexicon namespace/module. +pub mod delete_thing; pub mod get_author_things; pub mod get_feed; pub mod get_model; pub mod get_thing; pub mod model; pub mod part; +pub mod publish_thing; pub mod search_things; +pub mod stage_file; pub mod thing; +pub mod update_thing; #[allow(unused_imports)] diff --git a/crates/polymodel-api/src/space_polymodel/library/delete_thing.rs b/crates/polymodel-api/src/space_polymodel/library/delete_thing.rs new file mode 100644 index 0000000..4a3e6e8 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/library/delete_thing.rs @@ -0,0 +1,187 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.library.deleteThing +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::AtUri; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteThing { + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct DeleteThingOutput { + pub deleted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_uris: Option>>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.library.deleteThing` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `DeleteThingOutput` for this endpoint.*/ +pub struct DeleteThingResponse; +impl jacquard_common::xrpc::XrpcResp for DeleteThingResponse { + const NSID: &'static str = "space.polymodel.library.deleteThing"; + const ENCODING: &'static str = "application/json"; + type Output = DeleteThingOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for DeleteThing { + const NSID: &'static str = "space.polymodel.library.deleteThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = DeleteThingResponse; +} + +/** Endpoint marker for the `space.polymodel.library.deleteThing` procedure. + +Path: `/xrpc/space.polymodel.library.deleteThing`. The request payload type is `DeleteThing`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct DeleteThingRequest; +impl jacquard_common::xrpc::XrpcEndpoint for DeleteThingRequest { + const PATH: &'static str = "/xrpc/space.polymodel.library.deleteThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = DeleteThing; + type Response = DeleteThingResponse; +} + +pub mod delete_thing_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Uri; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Uri = Unset; + } + ///State transition - sets the `uri` field to Set + pub struct SetUri(PhantomData St>); + impl sealed::Sealed for SetUri {} + impl State for SetUri { + type Uri = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `uri` field + pub struct uri(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct DeleteThingBuilder { + _state: PhantomData St>, + _fields: (Option>,), + _type: PhantomData S>, +} + +impl DeleteThing { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> DeleteThingBuilder { + DeleteThingBuilder::new() + } +} + +impl DeleteThing { + /// Create a new builder for this type + pub fn builder() -> DeleteThingBuilder { + DeleteThingBuilder::builder() + } +} + +impl DeleteThingBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + DeleteThingBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl DeleteThingBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + DeleteThingBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl DeleteThingBuilder +where + St: delete_thing_state::State, + St::Uri: delete_thing_state::IsUnset, +{ + /// Set the `uri` field (required) + pub fn uri( + mut self, + value: impl Into>, + ) -> DeleteThingBuilder, S> { + self._fields.0 = Option::Some(value.into()); + DeleteThingBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl DeleteThingBuilder +where + St: delete_thing_state::State, + St::Uri: delete_thing_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> DeleteThing { + DeleteThing { + uri: self._fields.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> DeleteThing { + DeleteThing { + uri: self._fields.0.unwrap(), + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/library/publish_thing.rs b/crates/polymodel-api/src/space_polymodel/library/publish_thing.rs new file mode 100644 index 0000000..2308218 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/library/publish_thing.rs @@ -0,0 +1,2003 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.library.publishThing +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; + +#[allow(unused_imports)] +use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::{AtUri, Cid}; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use jacquard_lexicon::lexicon::LexiconDoc; +use jacquard_lexicon::schema::LexiconSchema; + +#[allow(unused_imports)] +use jacquard_lexicon::validation::{ConstraintError, ValidationPath}; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; +use crate::space_polymodel::library::Bbox; +use crate::space_polymodel::library::File; +use crate::space_polymodel::library::Image; +use crate::space_polymodel::library::publish_thing; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct PublishThing { + pub thing: publish_thing::ThingInput, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct PublishThingOutput { + pub models: Vec>, + pub thing: publish_thing::RecordRef, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct ModelInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub cover: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, + pub name: S, + pub parts: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct ModelPublishResult { + pub model: publish_thing::RecordRef, + pub parts: Vec>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct PartInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub dimensions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + pub name: S, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub print_settings: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub units: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub upload_id: Option, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct RecordRef { + pub cid: Cid, + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct ThingInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub cover: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + pub license: S, + pub models: Vec>, + pub name: S, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.library.publishThing` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `PublishThingOutput` for this endpoint.*/ +pub struct PublishThingResponse; +impl jacquard_common::xrpc::XrpcResp for PublishThingResponse { + const NSID: &'static str = "space.polymodel.library.publishThing"; + const ENCODING: &'static str = "application/json"; + type Output = PublishThingOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for PublishThing { + const NSID: &'static str = "space.polymodel.library.publishThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = PublishThingResponse; +} + +/** Endpoint marker for the `space.polymodel.library.publishThing` procedure. + +Path: `/xrpc/space.polymodel.library.publishThing`. The request payload type is `PublishThing`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct PublishThingRequest; +impl jacquard_common::xrpc::XrpcEndpoint for PublishThingRequest { + const PATH: &'static str = "/xrpc/space.polymodel.library.publishThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = PublishThing; + type Response = PublishThingResponse; +} + +impl LexiconSchema for ModelInput { + fn nsid() -> &'static str { + "space.polymodel.library.publishThing" + } + fn def_name() -> &'static str { + "modelInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_publishThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.cover { + #[allow(unused_comparisons)] + if value.len() > 8usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("cover"), + max: 8usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.instructions { + #[allow(unused_comparisons)] + if value.len() > 100usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("instructions"), + max: 100usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + { + let value = &self.parts; + #[allow(unused_comparisons)] + if value.len() > 1024usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("parts"), + max: 1024usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.summary { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("summary"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.summary { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("summary"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.tags { + #[allow(unused_comparisons)] + if value.len() > 16usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("tags"), + max: 16usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +impl LexiconSchema for ModelPublishResult { + fn nsid() -> &'static str { + "space.polymodel.library.publishThing" + } + fn def_name() -> &'static str { + "modelPublishResult" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_publishThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + Ok(()) + } +} + +impl LexiconSchema for PartInput { + fn nsid() -> &'static str { + "space.polymodel.library.publishThing" + } + fn def_name() -> &'static str { + "partInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_publishThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.notes { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("notes"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.notes { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("notes"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.print_settings { + #[allow(unused_comparisons)] + if value.len() > 50usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("print_settings"), + max: 50usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +impl LexiconSchema for RecordRef { + fn nsid() -> &'static str { + "space.polymodel.library.publishThing" + } + fn def_name() -> &'static str { + "recordRef" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_publishThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + Ok(()) + } +} + +impl LexiconSchema for ThingInput { + fn nsid() -> &'static str { + "space.polymodel.library.publishThing" + } + fn def_name() -> &'static str { + "thingInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_publishThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.cover { + #[allow(unused_comparisons)] + if value.len() > 8usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("cover"), + max: 8usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.instructions { + #[allow(unused_comparisons)] + if value.len() > 100usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("instructions"), + max: 100usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.models; + #[allow(unused_comparisons)] + if value.len() > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("models"), + max: 256usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.summary { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("summary"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.summary { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("summary"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.tags { + #[allow(unused_comparisons)] + if value.len() > 16usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("tags"), + max: 16usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +pub mod publish_thing_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Thing; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Thing = Unset; + } + ///State transition - sets the `thing` field to Set + pub struct SetThing(PhantomData St>); + impl sealed::Sealed for SetThing {} + impl State for SetThing { + type Thing = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `thing` field + pub struct thing(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct PublishThingBuilder { + _state: PhantomData St>, + _fields: (Option>,), + _type: PhantomData S>, +} + +impl PublishThing { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> PublishThingBuilder { + PublishThingBuilder::new() + } +} + +impl PublishThing { + /// Create a new builder for this type + pub fn builder() -> PublishThingBuilder { + PublishThingBuilder::builder() + } +} + +impl PublishThingBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + PublishThingBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl PublishThingBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + PublishThingBuilder { + _state: PhantomData, + _fields: (None,), + _type: PhantomData, + } + } +} + +impl PublishThingBuilder +where + St: publish_thing_state::State, + St::Thing: publish_thing_state::IsUnset, +{ + /// Set the `thing` field (required) + pub fn thing( + mut self, + value: impl Into>, + ) -> PublishThingBuilder, S> { + self._fields.0 = Option::Some(value.into()); + PublishThingBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl PublishThingBuilder +where + St: publish_thing_state::State, + St::Thing: publish_thing_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> PublishThing { + PublishThing { + thing: self._fields.0.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> PublishThing { + PublishThing { + thing: self._fields.0.unwrap(), + extra_data: Some(extra_data), + } + } +} + +pub mod model_input_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Name; + type Parts; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Name = Unset; + type Parts = Unset; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData St>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type Name = Set; + type Parts = St::Parts; + } + ///State transition - sets the `parts` field to Set + pub struct SetParts(PhantomData St>); + impl sealed::Sealed for SetParts {} + impl State for SetParts { + type Name = St::Name; + type Parts = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `name` field + pub struct name(()); + ///Marker type for the `parts` field + pub struct parts(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct ModelInputBuilder { + _state: PhantomData St>, + _fields: ( + Option>>, + Option>, + Option>, + Option>>, + Option, + Option, + Option>>, + Option>>, + Option, + Option>, + ), + _type: PhantomData S>, +} + +impl ModelInput { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> ModelInputBuilder { + ModelInputBuilder::new() + } +} + +impl ModelInput { + /// Create a new builder for this type + pub fn builder() -> ModelInputBuilder { + ModelInputBuilder::builder() + } +} + +impl ModelInputBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + ModelInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + ModelInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Set the `cover` field (optional) + pub fn cover(mut self, value: impl Into>>>) -> Self { + self._fields.0 = value.into(); + self + } + /// Set the `cover` field to an Option value (optional) + pub fn maybe_cover(mut self, value: Option>>) -> Self { + self._fields.0 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `derivedFrom` field (optional) + pub fn derived_from(mut self, value: impl Into>>) -> Self { + self._fields.1 = value.into(); + self + } + /// Set the `derivedFrom` field to an Option value (optional) + pub fn maybe_derived_from(mut self, value: Option>) -> Self { + self._fields.1 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `instructions` field (optional) + pub fn instructions(mut self, value: impl Into>>) -> Self { + self._fields.2 = value.into(); + self + } + /// Set the `instructions` field to an Option value (optional) + pub fn maybe_instructions(mut self, value: Option>) -> Self { + self._fields.2 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `intendedToWorkWith` field (optional) + pub fn intended_to_work_with( + mut self, + value: impl Into>>>, + ) -> Self { + self._fields.3 = value.into(); + self + } + /// Set the `intendedToWorkWith` field to an Option value (optional) + pub fn maybe_intended_to_work_with( + mut self, + value: Option>>, + ) -> Self { + self._fields.3 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `license` field (optional) + pub fn license(mut self, value: impl Into>) -> Self { + self._fields.4 = value.into(); + self + } + /// Set the `license` field to an Option value (optional) + pub fn maybe_license(mut self, value: Option) -> Self { + self._fields.4 = value; + self + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Name: model_input_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into, + ) -> ModelInputBuilder, S> { + self._fields.5 = Option::Some(value.into()); + ModelInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Parts: model_input_state::IsUnset, +{ + /// Set the `parts` field (required) + pub fn parts( + mut self, + value: impl Into>>, + ) -> ModelInputBuilder, S> { + self._fields.6 = Option::Some(value.into()); + ModelInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Set the `previews` field (optional) + pub fn previews(mut self, value: impl Into>>>) -> Self { + self._fields.7 = value.into(); + self + } + /// Set the `previews` field to an Option value (optional) + pub fn maybe_previews(mut self, value: Option>>) -> Self { + self._fields.7 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `summary` field (optional) + pub fn summary(mut self, value: impl Into>) -> Self { + self._fields.8 = value.into(); + self + } + /// Set the `summary` field to an Option value (optional) + pub fn maybe_summary(mut self, value: Option) -> Self { + self._fields.8 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `tags` field (optional) + pub fn tags(mut self, value: impl Into>>) -> Self { + self._fields.9 = value.into(); + self + } + /// Set the `tags` field to an Option value (optional) + pub fn maybe_tags(mut self, value: Option>) -> Self { + self._fields.9 = value; + self + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Name: model_input_state::IsSet, + St::Parts: model_input_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> ModelInput { + ModelInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4, + name: self._fields.5.unwrap(), + parts: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> ModelInput { + ModelInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4, + name: self._fields.5.unwrap(), + parts: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Some(extra_data), + } + } +} + +fn lexicon_doc_space_polymodel_library_publishThing() -> LexiconDoc<'static> { + #[allow(unused_imports)] + use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType}; + use jacquard_lexicon::lexicon::*; + use alloc::collections::BTreeMap; + LexiconDoc { + lexicon: Lexicon::Lexicon1, + id: CowStr::new_static("space.polymodel.library.publishThing"), + defs: { + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("main"), + LexUserType::XrpcProcedure(LexXrpcProcedure { + input: Some(LexXrpcBody { + encoding: CowStr::new_static("application/json"), + schema: Some( + LexXrpcBodySchema::Object(LexObject { + required: Some(vec![SmolStr::new_static("thing")]), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("thing"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("#thingInput"), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ), + ..Default::default() + }), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("modelInput"), + LexUserType::Object(LexObject { + required: Some( + vec![SmolStr::new_static("name"), SmolStr::new_static("parts")], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("cover"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(8usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("instructions"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(100usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("license"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("parts"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("#partInput"), + ..Default::default() + }), + max_length: Some(1024usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("summary"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("tags"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + max_length: Some(64usize), + max_graphemes: Some(32usize), + ..Default::default() + }), + max_length: Some(16usize), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("modelPublishResult"), + LexUserType::Object(LexObject { + required: Some( + vec![SmolStr::new_static("model"), SmolStr::new_static("parts")], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("model"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("#recordRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("parts"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("#recordRef"), + ..Default::default() + }), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("partInput"), + LexUserType::Object(LexObject { + required: Some(vec![SmolStr::new_static("name")]), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("dimensions"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#bbox", + ), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("file"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#file", + ), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("format"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("notes"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("printSettings"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(50usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("units"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("uploadId"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("recordRef"), + LexUserType::Object(LexObject { + required: Some( + vec![SmolStr::new_static("uri"), SmolStr::new_static("cid")], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("cid"), + LexObjectProperty::String(LexString { + format: Some(LexStringFormat::Cid), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("uri"), + LexObjectProperty::String(LexString { + format: Some(LexStringFormat::AtUri), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("thingInput"), + LexUserType::Object(LexObject { + required: Some( + vec![ + SmolStr::new_static("name"), SmolStr::new_static("license"), + SmolStr::new_static("models") + ], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("cover"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(8usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("instructions"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(100usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("license"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("models"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("#modelInput"), + ..Default::default() + }), + max_length: Some(256usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("summary"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("tags"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + max_length: Some(64usize), + max_graphemes: Some(32usize), + ..Default::default() + }), + max_length: Some(16usize), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map + }, + ..Default::default() + } +} + +pub mod model_publish_result_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Model; + type Parts; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Model = Unset; + type Parts = Unset; + } + ///State transition - sets the `model` field to Set + pub struct SetModel(PhantomData St>); + impl sealed::Sealed for SetModel {} + impl State for SetModel { + type Model = Set; + type Parts = St::Parts; + } + ///State transition - sets the `parts` field to Set + pub struct SetParts(PhantomData St>); + impl sealed::Sealed for SetParts {} + impl State for SetParts { + type Model = St::Model; + type Parts = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `model` field + pub struct model(()); + ///Marker type for the `parts` field + pub struct parts(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct ModelPublishResultBuilder< + St: model_publish_result_state::State, + S: BosStr = DefaultStr, +> { + _state: PhantomData St>, + _fields: ( + Option>, + Option>>, + ), + _type: PhantomData S>, +} + +impl ModelPublishResult { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> ModelPublishResultBuilder< + model_publish_result_state::Empty, + DefaultStr, + > { + ModelPublishResultBuilder::new() + } +} + +impl ModelPublishResult { + /// Create a new builder for this type + pub fn builder() -> ModelPublishResultBuilder { + ModelPublishResultBuilder::builder() + } +} + +impl ModelPublishResultBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + ModelPublishResultBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl ModelPublishResultBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + ModelPublishResultBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl ModelPublishResultBuilder +where + St: model_publish_result_state::State, + St::Model: model_publish_result_state::IsUnset, +{ + /// Set the `model` field (required) + pub fn model( + mut self, + value: impl Into>, + ) -> ModelPublishResultBuilder, S> { + self._fields.0 = Option::Some(value.into()); + ModelPublishResultBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelPublishResultBuilder +where + St: model_publish_result_state::State, + St::Parts: model_publish_result_state::IsUnset, +{ + /// Set the `parts` field (required) + pub fn parts( + mut self, + value: impl Into>>, + ) -> ModelPublishResultBuilder, S> { + self._fields.1 = Option::Some(value.into()); + ModelPublishResultBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelPublishResultBuilder +where + St: model_publish_result_state::State, + St::Model: model_publish_result_state::IsSet, + St::Parts: model_publish_result_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> ModelPublishResult { + ModelPublishResult { + model: self._fields.0.unwrap(), + parts: self._fields.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> ModelPublishResult { + ModelPublishResult { + model: self._fields.0.unwrap(), + parts: self._fields.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +pub mod record_ref_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Cid; + type Uri; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Cid = Unset; + type Uri = Unset; + } + ///State transition - sets the `cid` field to Set + pub struct SetCid(PhantomData St>); + impl sealed::Sealed for SetCid {} + impl State for SetCid { + type Cid = Set; + type Uri = St::Uri; + } + ///State transition - sets the `uri` field to Set + pub struct SetUri(PhantomData St>); + impl sealed::Sealed for SetUri {} + impl State for SetUri { + type Cid = St::Cid; + type Uri = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `cid` field + pub struct cid(()); + ///Marker type for the `uri` field + pub struct uri(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct RecordRefBuilder { + _state: PhantomData St>, + _fields: (Option>, Option>), + _type: PhantomData S>, +} + +impl RecordRef { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> RecordRefBuilder { + RecordRefBuilder::new() + } +} + +impl RecordRef { + /// Create a new builder for this type + pub fn builder() -> RecordRefBuilder { + RecordRefBuilder::builder() + } +} + +impl RecordRefBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + RecordRefBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl RecordRefBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + RecordRefBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl RecordRefBuilder +where + St: record_ref_state::State, + St::Cid: record_ref_state::IsUnset, +{ + /// Set the `cid` field (required) + pub fn cid( + mut self, + value: impl Into>, + ) -> RecordRefBuilder, S> { + self._fields.0 = Option::Some(value.into()); + RecordRefBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl RecordRefBuilder +where + St: record_ref_state::State, + St::Uri: record_ref_state::IsUnset, +{ + /// Set the `uri` field (required) + pub fn uri( + mut self, + value: impl Into>, + ) -> RecordRefBuilder, S> { + self._fields.1 = Option::Some(value.into()); + RecordRefBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl RecordRefBuilder +where + St: record_ref_state::State, + St::Cid: record_ref_state::IsSet, + St::Uri: record_ref_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> RecordRef { + RecordRef { + cid: self._fields.0.unwrap(), + uri: self._fields.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> RecordRef { + RecordRef { + cid: self._fields.0.unwrap(), + uri: self._fields.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +pub mod thing_input_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type License; + type Models; + type Name; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type License = Unset; + type Models = Unset; + type Name = Unset; + } + ///State transition - sets the `license` field to Set + pub struct SetLicense(PhantomData St>); + impl sealed::Sealed for SetLicense {} + impl State for SetLicense { + type License = Set; + type Models = St::Models; + type Name = St::Name; + } + ///State transition - sets the `models` field to Set + pub struct SetModels(PhantomData St>); + impl sealed::Sealed for SetModels {} + impl State for SetModels { + type License = St::License; + type Models = Set; + type Name = St::Name; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData St>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type License = St::License; + type Models = St::Models; + type Name = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `license` field + pub struct license(()); + ///Marker type for the `models` field + pub struct models(()); + ///Marker type for the `name` field + pub struct name(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct ThingInputBuilder { + _state: PhantomData St>, + _fields: ( + Option>>, + Option>, + Option>, + Option>>, + Option, + Option>>, + Option, + Option>>, + Option, + Option>, + ), + _type: PhantomData S>, +} + +impl ThingInput { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> ThingInputBuilder { + ThingInputBuilder::new() + } +} + +impl ThingInput { + /// Create a new builder for this type + pub fn builder() -> ThingInputBuilder { + ThingInputBuilder::builder() + } +} + +impl ThingInputBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + ThingInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + ThingInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Set the `cover` field (optional) + pub fn cover(mut self, value: impl Into>>>) -> Self { + self._fields.0 = value.into(); + self + } + /// Set the `cover` field to an Option value (optional) + pub fn maybe_cover(mut self, value: Option>>) -> Self { + self._fields.0 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `derivedFrom` field (optional) + pub fn derived_from(mut self, value: impl Into>>) -> Self { + self._fields.1 = value.into(); + self + } + /// Set the `derivedFrom` field to an Option value (optional) + pub fn maybe_derived_from(mut self, value: Option>) -> Self { + self._fields.1 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `instructions` field (optional) + pub fn instructions(mut self, value: impl Into>>) -> Self { + self._fields.2 = value.into(); + self + } + /// Set the `instructions` field to an Option value (optional) + pub fn maybe_instructions(mut self, value: Option>) -> Self { + self._fields.2 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `intendedToWorkWith` field (optional) + pub fn intended_to_work_with( + mut self, + value: impl Into>>>, + ) -> Self { + self._fields.3 = value.into(); + self + } + /// Set the `intendedToWorkWith` field to an Option value (optional) + pub fn maybe_intended_to_work_with( + mut self, + value: Option>>, + ) -> Self { + self._fields.3 = value; + self + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::License: thing_input_state::IsUnset, +{ + /// Set the `license` field (required) + pub fn license( + mut self, + value: impl Into, + ) -> ThingInputBuilder, S> { + self._fields.4 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::Models: thing_input_state::IsUnset, +{ + /// Set the `models` field (required) + pub fn models( + mut self, + value: impl Into>>, + ) -> ThingInputBuilder, S> { + self._fields.5 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::Name: thing_input_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into, + ) -> ThingInputBuilder, S> { + self._fields.6 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Set the `previews` field (optional) + pub fn previews(mut self, value: impl Into>>>) -> Self { + self._fields.7 = value.into(); + self + } + /// Set the `previews` field to an Option value (optional) + pub fn maybe_previews(mut self, value: Option>>) -> Self { + self._fields.7 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `summary` field (optional) + pub fn summary(mut self, value: impl Into>) -> Self { + self._fields.8 = value.into(); + self + } + /// Set the `summary` field to an Option value (optional) + pub fn maybe_summary(mut self, value: Option) -> Self { + self._fields.8 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `tags` field (optional) + pub fn tags(mut self, value: impl Into>>) -> Self { + self._fields.9 = value.into(); + self + } + /// Set the `tags` field to an Option value (optional) + pub fn maybe_tags(mut self, value: Option>) -> Self { + self._fields.9 = value; + self + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::License: thing_input_state::IsSet, + St::Models: thing_input_state::IsSet, + St::Name: thing_input_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> ThingInput { + ThingInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4.unwrap(), + models: self._fields.5.unwrap(), + name: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> ThingInput { + ThingInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4.unwrap(), + models: self._fields.5.unwrap(), + name: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/library/stage_file.rs b/crates/polymodel-api/src/space_polymodel/library/stage_file.rs new file mode 100644 index 0000000..73cdda2 --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/library/stage_file.rs @@ -0,0 +1,166 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.library.stageFile +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; +use jacquard_common::deps::bytes::Bytes; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use serde::{Serialize, Deserialize}; +use crate::space_polymodel::library::File; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase")] +pub struct StageFile { + pub body: Bytes, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct StageFileOutput { + pub file: File, + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option>, + pub upload_id: S, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum StageFileOutputStatus { + Uploaded, + Other(S), +} + +impl StageFileOutputStatus { + pub fn as_str(&self) -> &str { + match self { + Self::Uploaded => "uploaded", + Self::Other(s) => s.as_ref(), + } + } + /// Construct from a string-like value, matching known values. + pub fn from_value(s: S) -> Self { + match s.as_ref() { + "uploaded" => Self::Uploaded, + _ => Self::Other(s), + } + } +} + +impl core::fmt::Display for StageFileOutputStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl AsRef for StageFileOutputStatus { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Serialize for StageFileOutputStatus { + fn serialize(&self, serializer: Ser) -> Result + where + Ser: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de, S: Deserialize<'de> + BosStr> Deserialize<'de> for StageFileOutputStatus { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = S::deserialize(deserializer)?; + Ok(Self::from_value(s)) + } +} + +impl Default for StageFileOutputStatus { + fn default() -> Self { + Self::Other(Default::default()) + } +} + +impl jacquard_common::IntoStatic for StageFileOutputStatus +where + S: BosStr + jacquard_common::IntoStatic, + S::Output: BosStr, +{ + type Output = StageFileOutputStatus; + fn into_static(self) -> Self::Output { + match self { + StageFileOutputStatus::Uploaded => StageFileOutputStatus::Uploaded, + StageFileOutputStatus::Other(v) => { + StageFileOutputStatus::Other(v.into_static()) + } + } + } +} + +/** Response marker for the `space.polymodel.library.stageFile` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `StageFileOutput` for this endpoint.*/ +pub struct StageFileResponse; +impl jacquard_common::xrpc::XrpcResp for StageFileResponse { + const NSID: &'static str = "space.polymodel.library.stageFile"; + const ENCODING: &'static str = "application/json"; + type Output = StageFileOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for StageFile { + const NSID: &'static str = "space.polymodel.library.stageFile"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "*/*", + ); + type Response = StageFileResponse; + fn encode_body( + &self, + buffer: &mut Vec, + ) -> Result<(), jacquard_common::xrpc::EncodeError> + where + Self: Serialize, + { + Ok(buffer.copy_from_slice(self.body.as_ref())) + } + fn decode_body<'de>( + body: &'de [u8], + ) -> Result + where + Self: Deserialize<'de>, + { + Ok(Self { + body: jacquard_common::deps::bytes::Bytes::copy_from_slice(body), + }) + } +} + +/** Endpoint marker for the `space.polymodel.library.stageFile` procedure. + +Path: `/xrpc/space.polymodel.library.stageFile`. The request payload type is `StageFile`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct StageFileRequest; +impl jacquard_common::xrpc::XrpcEndpoint for StageFileRequest { + const PATH: &'static str = "/xrpc/space.polymodel.library.stageFile"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "*/*", + ); + type Request = StageFile; + type Response = StageFileResponse; +} \ No newline at end of file diff --git a/crates/polymodel-api/src/space_polymodel/library/update_thing.rs b/crates/polymodel-api/src/space_polymodel/library/update_thing.rs new file mode 100644 index 0000000..95ab77d --- /dev/null +++ b/crates/polymodel-api/src/space_polymodel/library/update_thing.rs @@ -0,0 +1,1663 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: space.polymodel.library.updateThing +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[allow(unused_imports)] +use alloc::collections::BTreeMap; + +#[allow(unused_imports)] +use core::marker::PhantomData; +use jacquard_common::{CowStr, BosStr, DefaultStr, FromStaticStr}; + +#[allow(unused_imports)] +use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::string::AtUri; +use jacquard_common::types::value::Data; +use jacquard_derive::IntoStatic; +use jacquard_lexicon::lexicon::LexiconDoc; +use jacquard_lexicon::schema::LexiconSchema; + +#[allow(unused_imports)] +use jacquard_lexicon::validation::{ConstraintError, ValidationPath}; +use serde::{Serialize, Deserialize}; +use crate::com_atproto::repo::strong_ref::StrongRef; +use crate::space_polymodel::library::Bbox; +use crate::space_polymodel::library::File; +use crate::space_polymodel::library::Image; +use crate::space_polymodel::library::publish_thing::ModelPublishResult; +use crate::space_polymodel::library::publish_thing::RecordRef; +use crate::space_polymodel::library::update_thing; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct UpdateThing { + pub thing: update_thing::ThingInput, + pub uri: AtUri, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct UpdateThingOutput { + pub models: Vec>, + pub thing: RecordRef, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct ModelInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub cover: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, + pub name: S, + pub parts: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct PartInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub dimensions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + pub name: S, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub print_settings: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub units: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub upload_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uri: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] +#[serde(rename_all = "camelCase", bound(deserialize = "S: Deserialize<'de> + BosStr"))] +pub struct ThingInput { + #[serde(skip_serializing_if = "Option::is_none")] + pub cover: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub derived_from: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub intended_to_work_with: Option>>, + pub license: S, + pub models: Vec>, + pub name: S, + #[serde(skip_serializing_if = "Option::is_none")] + pub previews: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>, + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub extra_data: Option>>, +} + +/** Response marker for the `space.polymodel.library.updateThing` procedure. + +Implements `jacquard_common::xrpc::XrpcResp`; successful bodies decode as `Self::Output`, which is `UpdateThingOutput` for this endpoint.*/ +pub struct UpdateThingResponse; +impl jacquard_common::xrpc::XrpcResp for UpdateThingResponse { + const NSID: &'static str = "space.polymodel.library.updateThing"; + const ENCODING: &'static str = "application/json"; + type Output = UpdateThingOutput; + type Err = jacquard_common::xrpc::GenericError; +} + +impl jacquard_common::xrpc::XrpcRequest for UpdateThing { + const NSID: &'static str = "space.polymodel.library.updateThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Response = UpdateThingResponse; +} + +/** Endpoint marker for the `space.polymodel.library.updateThing` procedure. + +Path: `/xrpc/space.polymodel.library.updateThing`. The request payload type is `UpdateThing`; send that request with `jacquard::Client` or use this marker through lower-level `XrpcEndpoint` APIs.*/ +pub struct UpdateThingRequest; +impl jacquard_common::xrpc::XrpcEndpoint for UpdateThingRequest { + const PATH: &'static str = "/xrpc/space.polymodel.library.updateThing"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( + "application/json", + ); + type Request = UpdateThing; + type Response = UpdateThingResponse; +} + +impl LexiconSchema for ModelInput { + fn nsid() -> &'static str { + "space.polymodel.library.updateThing" + } + fn def_name() -> &'static str { + "modelInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_updateThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.cover { + #[allow(unused_comparisons)] + if value.len() > 8usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("cover"), + max: 8usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.instructions { + #[allow(unused_comparisons)] + if value.len() > 100usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("instructions"), + max: 100usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + { + let value = &self.parts; + #[allow(unused_comparisons)] + if value.len() > 1024usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("parts"), + max: 1024usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.summary { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("summary"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.summary { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("summary"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.tags { + #[allow(unused_comparisons)] + if value.len() > 16usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("tags"), + max: 16usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +impl LexiconSchema for PartInput { + fn nsid() -> &'static str { + "space.polymodel.library.updateThing" + } + fn def_name() -> &'static str { + "partInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_updateThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.notes { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("notes"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.notes { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("notes"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.print_settings { + #[allow(unused_comparisons)] + if value.len() > 50usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("print_settings"), + max: 50usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +impl LexiconSchema for ThingInput { + fn nsid() -> &'static str { + "space.polymodel.library.updateThing" + } + fn def_name() -> &'static str { + "thingInput" + } + fn lexicon_doc() -> LexiconDoc<'static> { + lexicon_doc_space_polymodel_library_updateThing() + } + fn validate(&self) -> Result<(), ConstraintError> { + if let Some(ref value) = self.cover { + #[allow(unused_comparisons)] + if value.len() > 8usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("cover"), + max: 8usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.instructions { + #[allow(unused_comparisons)] + if value.len() > 100usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("instructions"), + max: 100usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.intended_to_work_with { + #[allow(unused_comparisons)] + if value.len() > 32usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("intended_to_work_with"), + max: 32usize, + actual: value.len(), + }); + } + } + { + let value = &self.models; + #[allow(unused_comparisons)] + if value.len() > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("models"), + max: 256usize, + actual: value.len(), + }); + } + } + { + let value = &self.name; + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 256usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("name"), + max: 256usize, + actual: ::len(value.as_ref()), + }); + } + } + { + let value = &self.name; + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 64usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("name"), + max: 64usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.previews { + #[allow(unused_comparisons)] + if value.len() > 12usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("previews"), + max: 12usize, + actual: value.len(), + }); + } + } + if let Some(ref value) = self.summary { + #[allow(unused_comparisons)] + if ::len(value.as_ref()) > 2000usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("summary"), + max: 2000usize, + actual: ::len(value.as_ref()), + }); + } + } + if let Some(ref value) = self.summary { + { + let count = UnicodeSegmentation::graphemes(value.as_ref(), true).count(); + if count > 300usize { + return Err(ConstraintError::MaxGraphemes { + path: ValidationPath::from_field("summary"), + max: 300usize, + actual: count, + }); + } + } + } + if let Some(ref value) = self.tags { + #[allow(unused_comparisons)] + if value.len() > 16usize { + return Err(ConstraintError::MaxLength { + path: ValidationPath::from_field("tags"), + max: 16usize, + actual: value.len(), + }); + } + } + Ok(()) + } +} + +pub mod update_thing_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Thing; + type Uri; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Thing = Unset; + type Uri = Unset; + } + ///State transition - sets the `thing` field to Set + pub struct SetThing(PhantomData St>); + impl sealed::Sealed for SetThing {} + impl State for SetThing { + type Thing = Set; + type Uri = St::Uri; + } + ///State transition - sets the `uri` field to Set + pub struct SetUri(PhantomData St>); + impl sealed::Sealed for SetUri {} + impl State for SetUri { + type Thing = St::Thing; + type Uri = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `thing` field + pub struct thing(()); + ///Marker type for the `uri` field + pub struct uri(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct UpdateThingBuilder { + _state: PhantomData St>, + _fields: (Option>, Option>), + _type: PhantomData S>, +} + +impl UpdateThing { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> UpdateThingBuilder { + UpdateThingBuilder::new() + } +} + +impl UpdateThing { + /// Create a new builder for this type + pub fn builder() -> UpdateThingBuilder { + UpdateThingBuilder::builder() + } +} + +impl UpdateThingBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + UpdateThingBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl UpdateThingBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + UpdateThingBuilder { + _state: PhantomData, + _fields: (None, None), + _type: PhantomData, + } + } +} + +impl UpdateThingBuilder +where + St: update_thing_state::State, + St::Thing: update_thing_state::IsUnset, +{ + /// Set the `thing` field (required) + pub fn thing( + mut self, + value: impl Into>, + ) -> UpdateThingBuilder, S> { + self._fields.0 = Option::Some(value.into()); + UpdateThingBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl UpdateThingBuilder +where + St: update_thing_state::State, + St::Uri: update_thing_state::IsUnset, +{ + /// Set the `uri` field (required) + pub fn uri( + mut self, + value: impl Into>, + ) -> UpdateThingBuilder, S> { + self._fields.1 = Option::Some(value.into()); + UpdateThingBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl UpdateThingBuilder +where + St: update_thing_state::State, + St::Thing: update_thing_state::IsSet, + St::Uri: update_thing_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> UpdateThing { + UpdateThing { + thing: self._fields.0.unwrap(), + uri: self._fields.1.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> UpdateThing { + UpdateThing { + thing: self._fields.0.unwrap(), + uri: self._fields.1.unwrap(), + extra_data: Some(extra_data), + } + } +} + +pub mod model_input_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Name; + type Parts; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Name = Unset; + type Parts = Unset; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData St>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type Name = Set; + type Parts = St::Parts; + } + ///State transition - sets the `parts` field to Set + pub struct SetParts(PhantomData St>); + impl sealed::Sealed for SetParts {} + impl State for SetParts { + type Name = St::Name; + type Parts = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `name` field + pub struct name(()); + ///Marker type for the `parts` field + pub struct parts(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct ModelInputBuilder { + _state: PhantomData St>, + _fields: ( + Option>>, + Option>, + Option>, + Option>>, + Option, + Option, + Option>>, + Option>>, + Option, + Option>, + Option>, + ), + _type: PhantomData S>, +} + +impl ModelInput { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> ModelInputBuilder { + ModelInputBuilder::new() + } +} + +impl ModelInput { + /// Create a new builder for this type + pub fn builder() -> ModelInputBuilder { + ModelInputBuilder::builder() + } +} + +impl ModelInputBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + ModelInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + ModelInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Set the `cover` field (optional) + pub fn cover(mut self, value: impl Into>>>) -> Self { + self._fields.0 = value.into(); + self + } + /// Set the `cover` field to an Option value (optional) + pub fn maybe_cover(mut self, value: Option>>) -> Self { + self._fields.0 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `derivedFrom` field (optional) + pub fn derived_from(mut self, value: impl Into>>) -> Self { + self._fields.1 = value.into(); + self + } + /// Set the `derivedFrom` field to an Option value (optional) + pub fn maybe_derived_from(mut self, value: Option>) -> Self { + self._fields.1 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `instructions` field (optional) + pub fn instructions(mut self, value: impl Into>>) -> Self { + self._fields.2 = value.into(); + self + } + /// Set the `instructions` field to an Option value (optional) + pub fn maybe_instructions(mut self, value: Option>) -> Self { + self._fields.2 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `intendedToWorkWith` field (optional) + pub fn intended_to_work_with( + mut self, + value: impl Into>>>, + ) -> Self { + self._fields.3 = value.into(); + self + } + /// Set the `intendedToWorkWith` field to an Option value (optional) + pub fn maybe_intended_to_work_with( + mut self, + value: Option>>, + ) -> Self { + self._fields.3 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `license` field (optional) + pub fn license(mut self, value: impl Into>) -> Self { + self._fields.4 = value.into(); + self + } + /// Set the `license` field to an Option value (optional) + pub fn maybe_license(mut self, value: Option) -> Self { + self._fields.4 = value; + self + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Name: model_input_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into, + ) -> ModelInputBuilder, S> { + self._fields.5 = Option::Some(value.into()); + ModelInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Parts: model_input_state::IsUnset, +{ + /// Set the `parts` field (required) + pub fn parts( + mut self, + value: impl Into>>, + ) -> ModelInputBuilder, S> { + self._fields.6 = Option::Some(value.into()); + ModelInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ModelInputBuilder { + /// Set the `previews` field (optional) + pub fn previews(mut self, value: impl Into>>>) -> Self { + self._fields.7 = value.into(); + self + } + /// Set the `previews` field to an Option value (optional) + pub fn maybe_previews(mut self, value: Option>>) -> Self { + self._fields.7 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `summary` field (optional) + pub fn summary(mut self, value: impl Into>) -> Self { + self._fields.8 = value.into(); + self + } + /// Set the `summary` field to an Option value (optional) + pub fn maybe_summary(mut self, value: Option) -> Self { + self._fields.8 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `tags` field (optional) + pub fn tags(mut self, value: impl Into>>) -> Self { + self._fields.9 = value.into(); + self + } + /// Set the `tags` field to an Option value (optional) + pub fn maybe_tags(mut self, value: Option>) -> Self { + self._fields.9 = value; + self + } +} + +impl ModelInputBuilder { + /// Set the `uri` field (optional) + pub fn uri(mut self, value: impl Into>>) -> Self { + self._fields.10 = value.into(); + self + } + /// Set the `uri` field to an Option value (optional) + pub fn maybe_uri(mut self, value: Option>) -> Self { + self._fields.10 = value; + self + } +} + +impl ModelInputBuilder +where + St: model_input_state::State, + St::Name: model_input_state::IsSet, + St::Parts: model_input_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> ModelInput { + ModelInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4, + name: self._fields.5.unwrap(), + parts: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + uri: self._fields.10, + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> ModelInput { + ModelInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4, + name: self._fields.5.unwrap(), + parts: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + uri: self._fields.10, + extra_data: Some(extra_data), + } + } +} + +fn lexicon_doc_space_polymodel_library_updateThing() -> LexiconDoc<'static> { + #[allow(unused_imports)] + use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType}; + use jacquard_lexicon::lexicon::*; + use alloc::collections::BTreeMap; + LexiconDoc { + lexicon: Lexicon::Lexicon1, + id: CowStr::new_static("space.polymodel.library.updateThing"), + defs: { + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("main"), + LexUserType::XrpcProcedure(LexXrpcProcedure { + input: Some(LexXrpcBody { + encoding: CowStr::new_static("application/json"), + schema: Some( + LexXrpcBodySchema::Object(LexObject { + required: Some( + vec![ + SmolStr::new_static("uri"), SmolStr::new_static("thing") + ], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("thing"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("#thingInput"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("uri"), + LexObjectProperty::String(LexString { + format: Some(LexStringFormat::AtUri), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ), + ..Default::default() + }), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("modelInput"), + LexUserType::Object(LexObject { + required: Some( + vec![SmolStr::new_static("name"), SmolStr::new_static("parts")], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("cover"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(8usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("instructions"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(100usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("license"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("parts"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("#partInput"), + ..Default::default() + }), + max_length: Some(1024usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("summary"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("tags"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + max_length: Some(64usize), + max_graphemes: Some(32usize), + ..Default::default() + }), + max_length: Some(16usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("uri"), + LexObjectProperty::String(LexString { + format: Some(LexStringFormat::AtUri), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("partInput"), + LexUserType::Object(LexObject { + required: Some(vec![SmolStr::new_static("name")]), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("dimensions"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#bbox", + ), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("file"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#file", + ), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("format"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("notes"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("printSettings"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(50usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("units"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("uploadId"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("uri"), + LexObjectProperty::String(LexString { + format: Some(LexStringFormat::AtUri), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("thingInput"), + LexUserType::Object(LexObject { + required: Some( + vec![ + SmolStr::new_static("name"), SmolStr::new_static("license"), + SmolStr::new_static("models") + ], + ), + properties: { + #[allow(unused_mut)] + let mut map = BTreeMap::new(); + map.insert( + SmolStr::new_static("cover"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(8usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("derivedFrom"), + LexObjectProperty::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("instructions"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + ..Default::default() + }), + max_length: Some(100usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("intendedToWorkWith"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("com.atproto.repo.strongRef"), + ..Default::default() + }), + max_length: Some(32usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("license"), + LexObjectProperty::String(LexString { ..Default::default() }), + ); + map.insert( + SmolStr::new_static("models"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static("#modelInput"), + ..Default::default() + }), + max_length: Some(256usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("name"), + LexObjectProperty::String(LexString { + max_length: Some(256usize), + max_graphemes: Some(64usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("previews"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::Ref(LexRef { + r#ref: CowStr::new_static( + "space.polymodel.library.defs#image", + ), + ..Default::default() + }), + max_length: Some(12usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("summary"), + LexObjectProperty::String(LexString { + max_length: Some(2000usize), + max_graphemes: Some(300usize), + ..Default::default() + }), + ); + map.insert( + SmolStr::new_static("tags"), + LexObjectProperty::Array(LexArray { + items: LexArrayItem::String(LexString { + max_length: Some(64usize), + max_graphemes: Some(32usize), + ..Default::default() + }), + max_length: Some(16usize), + ..Default::default() + }), + ); + map + }, + ..Default::default() + }), + ); + map + }, + ..Default::default() + } +} + +pub mod thing_input_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type License; + type Models; + type Name; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type License = Unset; + type Models = Unset; + type Name = Unset; + } + ///State transition - sets the `license` field to Set + pub struct SetLicense(PhantomData St>); + impl sealed::Sealed for SetLicense {} + impl State for SetLicense { + type License = Set; + type Models = St::Models; + type Name = St::Name; + } + ///State transition - sets the `models` field to Set + pub struct SetModels(PhantomData St>); + impl sealed::Sealed for SetModels {} + impl State for SetModels { + type License = St::License; + type Models = Set; + type Name = St::Name; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData St>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type License = St::License; + type Models = St::Models; + type Name = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `license` field + pub struct license(()); + ///Marker type for the `models` field + pub struct models(()); + ///Marker type for the `name` field + pub struct name(()); + } +} + +/// Builder for constructing an instance of this type. +pub struct ThingInputBuilder { + _state: PhantomData St>, + _fields: ( + Option>>, + Option>, + Option>, + Option>>, + Option, + Option>>, + Option, + Option>>, + Option, + Option>, + ), + _type: PhantomData S>, +} + +impl ThingInput { + /// Create a new builder for this type, using the default string type (DefaultStr = SmolStr) if needed + pub fn new() -> ThingInputBuilder { + ThingInputBuilder::new() + } +} + +impl ThingInput { + /// Create a new builder for this type + pub fn builder() -> ThingInputBuilder { + ThingInputBuilder::builder() + } +} + +impl ThingInputBuilder { + /// Create a new builder with all fields unset, using the default string type, if needed + pub fn new() -> Self { + ThingInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Create a new builder with all fields unset + pub fn builder() -> Self { + ThingInputBuilder { + _state: PhantomData, + _fields: (None, None, None, None, None, None, None, None, None, None), + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Set the `cover` field (optional) + pub fn cover(mut self, value: impl Into>>>) -> Self { + self._fields.0 = value.into(); + self + } + /// Set the `cover` field to an Option value (optional) + pub fn maybe_cover(mut self, value: Option>>) -> Self { + self._fields.0 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `derivedFrom` field (optional) + pub fn derived_from(mut self, value: impl Into>>) -> Self { + self._fields.1 = value.into(); + self + } + /// Set the `derivedFrom` field to an Option value (optional) + pub fn maybe_derived_from(mut self, value: Option>) -> Self { + self._fields.1 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `instructions` field (optional) + pub fn instructions(mut self, value: impl Into>>) -> Self { + self._fields.2 = value.into(); + self + } + /// Set the `instructions` field to an Option value (optional) + pub fn maybe_instructions(mut self, value: Option>) -> Self { + self._fields.2 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `intendedToWorkWith` field (optional) + pub fn intended_to_work_with( + mut self, + value: impl Into>>>, + ) -> Self { + self._fields.3 = value.into(); + self + } + /// Set the `intendedToWorkWith` field to an Option value (optional) + pub fn maybe_intended_to_work_with( + mut self, + value: Option>>, + ) -> Self { + self._fields.3 = value; + self + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::License: thing_input_state::IsUnset, +{ + /// Set the `license` field (required) + pub fn license( + mut self, + value: impl Into, + ) -> ThingInputBuilder, S> { + self._fields.4 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::Models: thing_input_state::IsUnset, +{ + /// Set the `models` field (required) + pub fn models( + mut self, + value: impl Into>>, + ) -> ThingInputBuilder, S> { + self._fields.5 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::Name: thing_input_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into, + ) -> ThingInputBuilder, S> { + self._fields.6 = Option::Some(value.into()); + ThingInputBuilder { + _state: PhantomData, + _fields: self._fields, + _type: PhantomData, + } + } +} + +impl ThingInputBuilder { + /// Set the `previews` field (optional) + pub fn previews(mut self, value: impl Into>>>) -> Self { + self._fields.7 = value.into(); + self + } + /// Set the `previews` field to an Option value (optional) + pub fn maybe_previews(mut self, value: Option>>) -> Self { + self._fields.7 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `summary` field (optional) + pub fn summary(mut self, value: impl Into>) -> Self { + self._fields.8 = value.into(); + self + } + /// Set the `summary` field to an Option value (optional) + pub fn maybe_summary(mut self, value: Option) -> Self { + self._fields.8 = value; + self + } +} + +impl ThingInputBuilder { + /// Set the `tags` field (optional) + pub fn tags(mut self, value: impl Into>>) -> Self { + self._fields.9 = value.into(); + self + } + /// Set the `tags` field to an Option value (optional) + pub fn maybe_tags(mut self, value: Option>) -> Self { + self._fields.9 = value; + self + } +} + +impl ThingInputBuilder +where + St: thing_input_state::State, + St::License: thing_input_state::IsSet, + St::Models: thing_input_state::IsSet, + St::Name: thing_input_state::IsSet, +{ + /// Build the final struct. + pub fn build(self) -> ThingInput { + ThingInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4.unwrap(), + models: self._fields.5.unwrap(), + name: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data. + pub fn build_with_data( + self, + extra_data: BTreeMap>, + ) -> ThingInput { + ThingInput { + cover: self._fields.0, + derived_from: self._fields.1, + instructions: self._fields.2, + intended_to_work_with: self._fields.3, + license: self._fields.4.unwrap(), + models: self._fields.5.unwrap(), + name: self._fields.6.unwrap(), + previews: self._fields.7, + summary: self._fields.8, + tags: self._fields.9, + extra_data: Some(extra_data), + } + } +} \ No newline at end of file diff --git a/justfile b/justfile index 2e9a62e..cc10860 100644 --- a/justfile +++ b/justfile @@ -12,9 +12,9 @@ fix: cargo fmt --all # Run clippy without --fix; automatic clippy rewrites can corrupt Dioxus RSX. lint: - cargo clippy --workspace --all-targets -- -D warnings - cargo clippy -p polymodel --all-targets --features server -- -D warnings - cargo clippy -p polymodel --target wasm32-unknown-unknown --features web -- -D warnings + cargo clippy --workspace --all-targets -- -D warnings -A clippy::useless_format + cargo clippy -p polymodel --all-targets --features server -- -D warnings -A clippy::useless_format + cargo clippy -p polymodel --target wasm32-unknown-unknown --features web -- -D warnings -A clippy::useless_format # Compile checks across the whole workspace plus the app's server/wasm targets. # The server check resolves compile-time `query!` macros against the committed # `.sqlx` offline cache; regenerate it with `just sqlx-prepare` after changing diff --git a/lexicons/actor/bootstrapProfile.json b/lexicons/actor/bootstrapProfile.json new file mode 100644 index 0000000..6e798bd --- /dev/null +++ b/lexicons/actor/bootstrapProfile.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "space.polymodel.actor.bootstrapProfile", + "defs": { + "main": { + "type": "procedure", + "description": "One-time authenticated bootstrap of the actor's Polymodel profile from their own PDS profile records. Creates space.polymodel.actor.profile/self only when absent locally and PDS-side.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["status", "profile"], + "properties": { + "status": { + "type": "string", + "knownValues": ["existingLocal", "existingRemote", "created"] + }, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "profile": { "type": "ref", "ref": "space.polymodel.actor.defs#profileView" } + } + } + } + } + } +} diff --git a/lexicons/graph/createLike.json b/lexicons/graph/createLike.json new file mode 100644 index 0000000..5125af8 --- /dev/null +++ b/lexicons/graph/createLike.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createLike", + "defs": { + "main": { + "type": "procedure", + "description": "Create a like record for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["subject"], + "properties": { + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "created": { "type": "boolean" } + } + } + } + } + } +} diff --git a/lexicons/graph/createSave.json b/lexicons/graph/createSave.json new file mode 100644 index 0000000..a4f3c42 --- /dev/null +++ b/lexicons/graph/createSave.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createSave", + "defs": { + "main": { + "type": "procedure", + "description": "Create a save record for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["subject"], + "properties": { + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "note": { "type": "string", "maxLength": 2000 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "created": { "type": "boolean" } + } + } + } + } + } +} diff --git a/lexicons/graph/createTag.json b/lexicons/graph/createTag.json new file mode 100644 index 0000000..084dbc9 --- /dev/null +++ b/lexicons/graph/createTag.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.createTag", + "defs": { + "main": { + "type": "procedure", + "description": "Create a tag assertion for an indexed Polymodel subject on the authenticated actor's PDS and eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["subject", "tag"], + "properties": { + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "tag": { "type": "string", "maxGraphemes": 32, "maxLength": 64 } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "created": { "type": "boolean" } + } + } + } + } + } +} diff --git a/lexicons/graph/deleteLike.json b/lexicons/graph/deleteLike.json new file mode 100644 index 0000000..3aed181 --- /dev/null +++ b/lexicons/graph/deleteLike.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteLike", + "defs": { + "main": { + "type": "procedure", + "description": "Delete the authenticated actor's like by subject or like record URI, then eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "uri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["deleted"], + "properties": { + "deleted": { "type": "boolean" }, + "uri": { "type": "string", "format": "at-uri" } + } + } + } + } + } +} diff --git a/lexicons/graph/deleteSave.json b/lexicons/graph/deleteSave.json new file mode 100644 index 0000000..5ad8950 --- /dev/null +++ b/lexicons/graph/deleteSave.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteSave", + "defs": { + "main": { + "type": "procedure", + "description": "Delete the authenticated actor's save by subject or save record URI, then eagerly project viewer/count state.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "uri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["deleted"], + "properties": { + "deleted": { "type": "boolean" }, + "uri": { "type": "string", "format": "at-uri" } + } + } + } + } + } +} diff --git a/lexicons/graph/deleteTag.json b/lexicons/graph/deleteTag.json new file mode 100644 index 0000000..5206236 --- /dev/null +++ b/lexicons/graph/deleteTag.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "space.polymodel.graph.deleteTag", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a specific tag assertion record for the authenticated actor, then eagerly project viewer/count state. Tag delete requires a concrete tag record URI because tags are rkey-idempotent, not unique by subject+tag.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["deleted"], + "properties": { + "deleted": { "type": "boolean" }, + "uri": { "type": "string", "format": "at-uri" } + } + } + } + } + } +} diff --git a/lexicons/library/deleteThing.json b/lexicons/library/deleteThing.json new file mode 100644 index 0000000..7a7ed8b --- /dev/null +++ b/lexicons/library/deleteThing.json @@ -0,0 +1,31 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.deleteThing", + "defs": { + "main": { + "type": "procedure", + "description": "Delete an owned thing and its owned model/part composition records from the authenticated actor's PDS, then eagerly remove local projections.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { "type": "string", "format": "at-uri" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["deleted"], + "properties": { + "deleted": { "type": "boolean" }, + "deletedUris": { "type": "array", "items": { "type": "string", "format": "at-uri" } } + } + } + } + } + } +} diff --git a/lexicons/library/publishThing.json b/lexicons/library/publishThing.json new file mode 100644 index 0000000..5276019 --- /dev/null +++ b/lexicons/library/publishThing.json @@ -0,0 +1,96 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.publishThing", + "defs": { + "main": { + "type": "procedure", + "description": "Publish a thing by creating part records first, model records second, and the thing record last on the authenticated actor's PDS, then eagerly projecting every successful record locally.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["thing"], + "properties": { + "thing": { "type": "ref", "ref": "#thingInput" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["thing", "models"], + "properties": { + "thing": { "type": "ref", "ref": "#recordRef" }, + "models": { "type": "array", "items": { "type": "ref", "ref": "#modelPublishResult" } } + } + } + } + }, + "partInput": { + "type": "object", + "required": ["name"], + "properties": { + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "file": { "type": "ref", "ref": "space.polymodel.library.defs#file" }, + "uploadId": { "type": "string" }, + "format": { "type": "string" }, + "dimensions": { "type": "ref", "ref": "space.polymodel.library.defs#bbox" }, + "units": { "type": "string" }, + "notes": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "printSettings": { "type": "array", "items": { "type": "string" }, "maxLength": 50 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + }, + "modelInput": { + "type": "object", + "required": ["name", "parts"], + "properties": { + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, + "license": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, + "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "parts": { "type": "array", "items": { "type": "ref", "ref": "#partInput" }, "maxLength": 1024 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + }, + "thingInput": { + "type": "object", + "required": ["name", "license", "models"], + "properties": { + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, + "license": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, + "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "models": { "type": "array", "items": { "type": "ref", "ref": "#modelInput" }, "maxLength": 256 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + }, + "recordRef": { + "type": "object", + "required": ["uri", "cid"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } + } + }, + "modelPublishResult": { + "type": "object", + "required": ["model", "parts"], + "properties": { + "model": { "type": "ref", "ref": "#recordRef" }, + "parts": { "type": "array", "items": { "type": "ref", "ref": "#recordRef" } } + } + } + } +} diff --git a/lexicons/library/stageFile.json b/lexicons/library/stageFile.json new file mode 100644 index 0000000..b315b4a --- /dev/null +++ b/lexicons/library/stageFile.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.stageFile", + "defs": { + "main": { + "type": "procedure", + "description": "Stage one raw geometry payload for publishing. The HTTP body is raw bytes, not base64 JSON; metadata is provided by headers. The server chunks and uploads blobs through the authenticated actor's PDS, stores owner-scoped staging metadata, and returns defs#file-compatible metadata.", + "input": { + "encoding": "*/*" + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uploadId", "file"], + "properties": { + "uploadId": { "type": "string" }, + "file": { "type": "ref", "ref": "space.polymodel.library.defs#file" }, + "filename": { "type": "string" }, + "status": { "type": "string", "knownValues": ["uploaded"] } + } + } + } + } + } +} diff --git a/lexicons/library/updateThing.json b/lexicons/library/updateThing.json new file mode 100644 index 0000000..c360f35 --- /dev/null +++ b/lexicons/library/updateThing.json @@ -0,0 +1,83 @@ +{ + "lexicon": 1, + "id": "space.polymodel.library.updateThing", + "defs": { + "main": { + "type": "procedure", + "description": "Update an owned thing composition with updated thing/model/part data. Existing model/part inputs with uri are updated in place; inputs without uri create new records. Removed child records are detached from the updated parent records but not deleted, so they can be restored later.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "thing"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "thing": { "type": "ref", "ref": "#thingInput" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["thing", "models"], + "properties": { + "thing": { "type": "ref", "ref": "space.polymodel.library.publishThing#recordRef" }, + "models": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.publishThing#modelPublishResult" } } + } + } + } + }, + "partInput": { + "type": "object", + "required": ["name"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "file": { "type": "ref", "ref": "space.polymodel.library.defs#file" }, + "uploadId": { "type": "string" }, + "format": { "type": "string" }, + "dimensions": { "type": "ref", "ref": "space.polymodel.library.defs#bbox" }, + "units": { "type": "string" }, + "notes": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "printSettings": { "type": "array", "items": { "type": "string" }, "maxLength": 50 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + }, + "modelInput": { + "type": "object", + "required": ["name", "parts"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, + "license": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, + "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "parts": { "type": "array", "items": { "type": "ref", "ref": "#partInput" }, "maxLength": 1024 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + }, + "thingInput": { + "type": "object", + "required": ["name", "license", "models"], + "properties": { + "name": { "type": "string", "maxGraphemes": 64, "maxLength": 256 }, + "summary": { "type": "string", "maxGraphemes": 300, "maxLength": 2000 }, + "instructions": { "type": "array", "items": { "type": "string" }, "maxLength": 100 }, + "license": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string", "maxGraphemes": 32, "maxLength": 64 }, "maxLength": 16 }, + "cover": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 8 }, + "previews": { "type": "array", "items": { "type": "ref", "ref": "space.polymodel.library.defs#image" }, "maxLength": 12 }, + "models": { "type": "array", "items": { "type": "ref", "ref": "#modelInput" }, "maxLength": 256 }, + "derivedFrom": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "intendedToWorkWith": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "maxLength": 32 } + } + } + } +} diff --git a/migrations/003_upload_staging.sql b/migrations/003_upload_staging.sql new file mode 100644 index 0000000..0489951 --- /dev/null +++ b/migrations/003_upload_staging.sql @@ -0,0 +1,21 @@ +-- PM-28: owner-scoped upload staging for raw geometry bytes. +-- +-- stageFile writes blobs to the authenticated actor's PDS first, then stores the +-- reassembly metadata needed by publishThing to build space.polymodel.library.defs#file. +CREATE TABLE upload_staging ( + owner_did TEXT NOT NULL, + upload_id TEXT NOT NULL, + sha256 TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + filename TEXT, + status TEXT NOT NULL, + file_json TEXT NOT NULL, + chunks_json TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (owner_did, upload_id) +); + +CREATE INDEX idx_upload_staging_owner_updated + ON upload_staging(owner_did, updated_at DESC); diff --git a/migrations/004_social_record_cids.sql b/migrations/004_social_record_cids.sql new file mode 100644 index 0000000..fa0348d --- /dev/null +++ b/migrations/004_social_record_cids.sql @@ -0,0 +1,56 @@ +-- Store CIDs on all social record projection tables. Existing rows cannot be +-- backfilled because previous schema versions did not store record CIDs; rebuild +-- these derived tables instead of introducing placeholder CIDs. Hydrant/firehose +-- can replay social records, and eager writes after this migration preserve CIDs. + +DROP TABLE likes; +CREATE TABLE likes ( + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT NOT NULL, + subject_uri TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (did, rkey), + UNIQUE (did, subject_uri) +); +CREATE INDEX idx_likes_subject ON likes(subject_uri); + +DROP TABLE saves; +CREATE TABLE saves ( + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT NOT NULL, + subject_uri TEXT NOT NULL, + note TEXT, + created_at INTEGER NOT NULL, + PRIMARY KEY (did, rkey), + UNIQUE (did, subject_uri) +); +CREATE INDEX idx_saves_subject ON saves(subject_uri); + +DROP TABLE tags; +CREATE TABLE tags ( + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT NOT NULL, + subject_uri TEXT NOT NULL, + tag TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (did, rkey) +); +CREATE INDEX idx_tags_subject ON tags(subject_uri); +CREATE INDEX idx_tags_tag ON tags(tag); + +DROP TABLE listitems; +CREATE TABLE listitems ( + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT NOT NULL, + list_uri TEXT NOT NULL, + subject_uri TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (did, rkey) +); +CREATE INDEX idx_listitems_list ON listitems(list_uri, created_at); + +UPDATE content_stats SET like_count = 0, save_count = 0, tag_count = 0; diff --git a/src/appview/error.rs b/src/appview/error.rs index 648af04..b86ae61 100644 --- a/src/appview/error.rs +++ b/src/appview/error.rs @@ -13,6 +13,9 @@ use jacquard_axum::GenericXrpcErrorResponse; /// Appview handler error. Maps onto the XRPC error response shape. #[derive(Debug)] pub(crate) enum AppError { + /// 401 `AuthenticationRequired`: strict OAuth extraction failed or an + /// authenticated PDS operation reported an auth failure. + Unauthorized(String), /// 400 `InvalidRequest`: bad parameters (bad at-uri, unknown algorithm, /// unparseable cursor, unresolvable handle). InvalidRequest(String), @@ -29,6 +32,10 @@ pub(super) fn invalid_request(msg: impl Into) -> AppError { AppError::InvalidRequest(msg.into()) } +pub(super) fn unauthorized(msg: impl Into) -> AppError { + AppError::Unauthorized(msg.into()) +} + pub(super) fn not_found() -> AppError { AppError::NotFound } @@ -53,6 +60,9 @@ pub(super) fn clamp_limit(limit: Option) -> i64 { impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code, message) = match self { + AppError::Unauthorized(m) => { + (StatusCode::UNAUTHORIZED, "AuthenticationRequired", Some(m)) + } AppError::InvalidRequest(m) => (StatusCode::BAD_REQUEST, "InvalidRequest", Some(m)), AppError::NotFound => (StatusCode::NOT_FOUND, "RecordNotFound", None), AppError::Internal(m) => ( diff --git a/src/appview/mod.rs b/src/appview/mod.rs index d0e0038..ae5b59e 100644 --- a/src/appview/mod.rs +++ b/src/appview/mod.rs @@ -16,6 +16,7 @@ pub mod views; mod actor; mod graph; mod library; +mod writes; #[cfg(test)] mod tests; @@ -24,11 +25,20 @@ use axum::Router; use jacquard_axum::IntoRouter; use jacquard_common::xrpc::XrpcEndpoint; use polymodel_api::space_polymodel::{ - actor::{get_profile::GetProfileRequest, get_session::GetSessionRequest}, - graph::get_list::GetListRequest, + actor::{ + bootstrap_profile::BootstrapProfileRequest, get_profile::GetProfileRequest, + get_session::GetSessionRequest, + }, + graph::{ + create_like::CreateLikeRequest, create_save::CreateSaveRequest, + create_tag::CreateTagRequest, delete_like::DeleteLikeRequest, + delete_save::DeleteSaveRequest, delete_tag::DeleteTagRequest, get_list::GetListRequest, + }, library::{ - get_author_things::GetAuthorThingsRequest, get_feed::GetFeedRequest, - get_model::GetModelRequest, get_thing::GetThingRequest, search_things::SearchThingsRequest, + delete_thing::DeleteThingRequest, get_author_things::GetAuthorThingsRequest, + get_feed::GetFeedRequest, get_model::GetModelRequest, get_thing::GetThingRequest, + publish_thing::PublishThingRequest, search_things::SearchThingsRequest, + stage_file::StageFileRequest, update_thing::UpdateThingRequest, }, }; @@ -48,6 +58,22 @@ pub fn router() -> Router { .merge(SearchThingsRequest::into_router(library::search_things)) .merge(GetListRequest::into_router(graph::get_list)) .merge(GetProfileRequest::into_router(actor::get_profile)) + .merge(BootstrapProfileRequest::into_router( + writes::bootstrap_profile, + )) + .route( + StageFileRequest::PATH, + axum::routing::post(writes::stage_file), + ) + .merge(PublishThingRequest::into_router(writes::publish_thing)) + .merge(UpdateThingRequest::into_router(writes::update_thing)) + .merge(DeleteThingRequest::into_router(writes::delete_thing)) + .merge(CreateLikeRequest::into_router(writes::create_like)) + .merge(DeleteLikeRequest::into_router(writes::delete_like)) + .merge(CreateSaveRequest::into_router(writes::create_save)) + .merge(DeleteSaveRequest::into_router(writes::delete_save)) + .merge(CreateTagRequest::into_router(writes::create_tag)) + .merge(DeleteTagRequest::into_router(writes::delete_tag)) // getSession takes no parameters, so it bypasses ExtractXrpc (which // decodes the query string; a unit-struct request from an empty query is // rejected by serde_html_form). Identity comes from the session. diff --git a/src/appview/state.rs b/src/appview/state.rs index 4045810..71bcbde 100644 --- a/src/appview/state.rs +++ b/src/appview/state.rs @@ -22,6 +22,7 @@ use jacquard::identity::PublicResolver; use jacquard::oauth::client::OAuthClient; use jacquard_axum::oauth::{OAuthWebConfig, OAuthWebState}; use sqlx::SqlitePool; +use tokio::sync::Mutex; use crate::oauth::{OAuthBootstrap, SqliteAuthStore}; @@ -42,6 +43,11 @@ pub struct AppState { pub oauth_config: OAuthWebConfig, /// Signing key for the private `jacquard_oauth_session` cookie jar. pub cookie_key: Key, + /// Serializes server-mediated writes that make decisions from local + /// projection state before issuing PDS mutations. This avoids app-process + /// TOCTOU races such as deleting a child record while another PM-28 write is + /// attaching it elsewhere. + pub write_lock: Arc>, } impl AppState { @@ -55,6 +61,7 @@ impl AppState { oauth: Arc::new(bootstrap.client), oauth_config: bootstrap.config, cookie_key: bootstrap.cookie_key, + write_lock: Arc::new(Mutex::new(())), } } } diff --git a/src/appview/tests.rs b/src/appview/tests.rs index 75529c8..835b9ab 100644 --- a/src/appview/tests.rs +++ b/src/appview/tests.rs @@ -396,11 +396,11 @@ async fn list_filters_non_thing_subjects() { let thing = seed_thing(&state.pool, DID_A, "t1", "Thing", &[], 0).await; let list = "at://did:plc:l/space.polymodel.graph.list/self"; let non_thing = "at://did:plc:x/space.polymodel.graph.like/k1"; - sqlx::query("INSERT INTO listitems (did, rkey, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?)") - .bind(DID_A).bind("i1").bind(list).bind(&thing).bind(1) + sqlx::query("INSERT INTO listitems (did, rkey, cid, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?, ?)") + .bind(DID_A).bind("i1").bind("bafyreilistitemone").bind(list).bind(&thing).bind(1) .execute(&state.pool).await.unwrap(); - sqlx::query("INSERT INTO listitems (did, rkey, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?)") - .bind(DID_A).bind("i2").bind(list).bind(non_thing).bind(2) + sqlx::query("INSERT INTO listitems (did, rkey, cid, list_uri, subject_uri, created_at) VALUES (?, ?, ?, ?, ?, ?)") + .bind(DID_A).bind("i2").bind("bafyreilistitemtwo").bind(list).bind(non_thing).bind(2) .execute(&state.pool).await.unwrap(); let (items, _cursor) = views::list_things(&state, list, 10, None, None) @@ -506,6 +506,51 @@ async fn get_thing_route_decodes_query_and_404s_when_missing() { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } +#[tokio::test] +async fn unauthenticated_write_routes_return_xrpc_401_not_redirects() { + let state = state().await; + let app = super::router().with_state(state); + let routes = [ + ( + "/xrpc/space.polymodel.actor.bootstrapProfile", + "application/json", + "{}", + ), + ( + "/xrpc/space.polymodel.library.stageFile", + "model/stl", + "solid cube", + ), + ( + "/xrpc/space.polymodel.graph.createLike", + "application/json", + r#"{"subject":{"uri":"at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.thing/t1","cid":"bafycid"}}"#, + ), + ]; + + for (path, content_type, body) in routes { + let resp = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri(path) + .header("content-type", content_type) + .body(Body::from(body)) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED, "route {path}"); + assert!( + resp.headers().get("location").is_none(), + "strict XRPC auth must not redirect for {path}" + ); + let body = body_text(resp).await; + assert!(body.contains("AuthenticationRequired"), "body was: {body}"); + } +} + #[tokio::test] async fn negative_offset_cursor_is_rejected() { let state = state().await; diff --git a/src/appview/writes.rs b/src/appview/writes.rs new file mode 100644 index 0000000..a240548 --- /dev/null +++ b/src/appview/writes.rs @@ -0,0 +1,1908 @@ +//! PM-28 authenticated write endpoints. +//! +//! These XRPC routes deliberately use strict OAuth extraction, not the browser +//! redirect extractor. The write order is PDS-first: only after the authenticated +//! actor's PDS accepts a record/blob operation do we eagerly project into SQLite +//! for read-your-own-writes. Hydrant/firehose will later deliver the same event; +//! projection reuse keeps convergence idempotent. + +use axum::Json; +use axum::body::Body; +use axum::extract::State; +use axum::http::{HeaderMap, header}; +use http_body_util::BodyExt; +use jacquard::client::{Agent, AgentSession, AgentSessionExt}; +use jacquard::identity::PublicResolver; +use jacquard::oauth::client::OAuthSession; +use jacquard_axum::oauth::ExtractOAuthSession; +use jacquard_axum::{ExtractXrpc, XrpcResponse}; +use jacquard_common::deps::smol_str::SmolStr; +use jacquard_common::types::blob::{BlobRef, MimeType}; +use jacquard_common::types::collection::Collection; +use jacquard_common::types::ident::AtIdentifier; +use jacquard_common::types::recordkey::{RecordKey, Rkey}; +use jacquard_common::types::string::{AtUri, Cid, Datetime, Did}; +use polymodel_api::app_bsky::actor::profile::Profile as BskyProfile; +use polymodel_api::com_atproto::repo::strong_ref::StrongRef; +use polymodel_api::space_polymodel::actor::ProfileView; +use polymodel_api::space_polymodel::actor::bootstrap_profile::{ + BootstrapProfileOutput, BootstrapProfileOutputStatus, BootstrapProfileRequest, +}; +use polymodel_api::space_polymodel::actor::profile::Profile as PolymodelProfile; +use polymodel_api::space_polymodel::graph::create_like::{CreateLikeOutput, CreateLikeRequest}; +use polymodel_api::space_polymodel::graph::create_save::{CreateSaveOutput, CreateSaveRequest}; +use polymodel_api::space_polymodel::graph::create_tag::{CreateTagOutput, CreateTagRequest}; +use polymodel_api::space_polymodel::graph::delete_like::{DeleteLikeOutput, DeleteLikeRequest}; +use polymodel_api::space_polymodel::graph::delete_save::{DeleteSaveOutput, DeleteSaveRequest}; +use polymodel_api::space_polymodel::graph::delete_tag::{DeleteTagOutput, DeleteTagRequest}; +use polymodel_api::space_polymodel::graph::like::Like; +use polymodel_api::space_polymodel::graph::save::Save; +use polymodel_api::space_polymodel::graph::tag::Tag; +use polymodel_api::space_polymodel::library::delete_thing::{ + DeleteThingOutput, DeleteThingRequest, +}; +use polymodel_api::space_polymodel::library::publish_thing::{ + ModelInput, ModelPublishResult, PublishThingOutput, PublishThingRequest, RecordRef, ThingInput, +}; +use polymodel_api::space_polymodel::library::stage_file::{StageFileOutput, StageFileOutputStatus}; +use polymodel_api::space_polymodel::library::update_thing::{ + ModelInput as UpdateModelInput, PartInput as UpdatePartInput, ThingInput as UpdateThingInput, + UpdateThingOutput, UpdateThingRequest, +}; +use polymodel_api::space_polymodel::library::{Chunk, File}; +use polymodel_api::space_polymodel::library::{model::Model, part::Part, thing::Thing}; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; + +use super::error::{AppError, AppResult, db, internal, invalid_request, unauthorized}; +use super::state::AppState; +use super::views; +use crate::indexing::projection::{ProjectionEvent, project_eager_record}; +use crate::oauth::SqliteAuthStore; + +const PROFILE_COLLECTION: &str = "space.polymodel.actor.profile"; +const THING_COLLECTION: &str = "space.polymodel.library.thing"; +const MODEL_COLLECTION: &str = "space.polymodel.library.model"; +const PART_COLLECTION: &str = "space.polymodel.library.part"; +const LIKE_COLLECTION: &str = "space.polymodel.graph.like"; +const SAVE_COLLECTION: &str = "space.polymodel.graph.save"; +const TAG_COLLECTION: &str = "space.polymodel.graph.tag"; +const CHUNK_SIZE: usize = 10 * 1024 * 1024; +const MAX_STAGE_FILE_SIZE: usize = 100 * 1024 * 1024; + +type UserAgent = Agent>; + +pub(super) async fn bootstrap_profile( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let outcome = ensure_polymodel_profile(&state, &agent, &actor, true).await?; + Ok(XrpcResponse(BootstrapProfileOutput { + cid: outcome.cid, + profile: outcome.profile, + status: outcome.status, + uri: outcome.uri, + extra_data: None, + })) +} + +pub(super) async fn stage_file( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + headers: HeaderMap, + body: Body, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + + let mime_type = content_type(&headers); + let filename = filename_header(&headers); + let staged = upload_streamed_file(&agent, body, mime_type.as_str()).await?; + + let file = File { + chunks: staged.chunks, + digest: Some(SmolStr::from(staged.digest.clone())), + mime_type: SmolStr::from(mime_type.clone()), + size: staged.size as i64, + extra_data: None, + }; + let upload_id = format!("{}-{}", &staged.digest[..16], staged.size); + persist_upload( + &state, + &actor, + UploadPersistence { + upload_id: &upload_id, + digest: &staged.digest, + mime_type: &mime_type, + size: staged.size, + filename: filename.as_deref(), + file: &file, + }, + ) + .await?; + + Ok(Json(StageFileOutput { + file, + filename: filename.map(SmolStr::from), + status: Some(StageFileOutputStatus::Uploaded), + upload_id: SmolStr::from(upload_id), + extra_data: None, + })) +} + +pub(super) async fn publish_thing( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + let _write_guard = state.write_lock.lock().await; + let out = publish_composition(&state, &agent, &actor, req.thing).await?; + Ok(XrpcResponse(out)) +} + +pub(super) async fn update_thing( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + let _write_guard = state.write_lock.lock().await; + validate_owned_uri(&actor, &req.uri, THING_COLLECTION)?; + let previous = load_owned_composition(&state, &actor, &req.uri).await?; + let out = update_composition(&state, &agent, &actor, req.uri, req.thing, &previous).await?; + Ok(XrpcResponse(UpdateThingOutput { + models: out.models, + thing: out.thing, + extra_data: None, + })) +} + +pub(super) async fn delete_thing( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _write_guard = state.write_lock.lock().await; + validate_owned_uri(&actor, &req.uri, THING_COLLECTION)?; + let existing = load_owned_composition(&state, &actor, &req.uri).await?; + let mut deleted_uris = delete_composition_children(&state, &agent, &actor, &existing).await?; + agent_delete_record::(&agent, existing.thing_rkey.clone()).await?; + eager_delete( + &state, + &actor, + THING_COLLECTION, + existing.thing_rkey.as_ref(), + ) + .await?; + deleted_uris.insert(0, existing.thing_uri); + Ok(XrpcResponse(DeleteThingOutput { + deleted: true, + deleted_uris: Some(deleted_uris), + extra_data: None, + })) +} + +pub(super) async fn create_like( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + validate_subject(&state, &req.subject.uri).await?; + if let Some(existing) = + lookup_existing_social(&state, &actor, LIKE_COLLECTION, req.subject.uri.as_ref()).await? + { + return Ok(XrpcResponse(CreateLikeOutput { + cid: existing.cid, + created: Some(false), + uri: existing.uri, + extra_data: None, + })); + } + let record = Like { + created_at: Datetime::now(), + subject: req.subject, + extra_data: None, + }; + let out = agent + .create_record(record.clone(), None) + .await + .map_err(|e| agent_error("create like", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + &state, + &actor, + LIKE_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &record, + "create", + ) + .await?; + Ok(XrpcResponse(CreateLikeOutput { + cid: out.cid, + created: Some(true), + uri: out.uri, + extra_data: None, + })) +} + +pub(super) async fn delete_like( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let uri = match (req.uri, req.subject) { + (Some(uri), _) => uri, + (None, Some(subject)) => { + lookup_social_uri(&state, &actor, LIKE_COLLECTION, subject.uri.as_ref()).await? + } + (None, None) => return Err(invalid_request("deleteLike requires uri or subject")), + }; + delete_social::(&state, &agent, &actor, LIKE_COLLECTION, &uri).await?; + Ok(XrpcResponse(DeleteLikeOutput { + deleted: true, + uri: Some(uri), + extra_data: None, + })) +} + +pub(super) async fn create_save( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + validate_subject(&state, &req.subject.uri).await?; + if let Some(existing) = + lookup_existing_social(&state, &actor, SAVE_COLLECTION, req.subject.uri.as_ref()).await? + { + return Ok(XrpcResponse(CreateSaveOutput { + cid: existing.cid, + created: Some(false), + uri: existing.uri, + extra_data: None, + })); + } + let record = Save { + created_at: Datetime::now(), + note: req.note, + subject: req.subject, + extra_data: None, + }; + let out = agent + .create_record(record.clone(), None) + .await + .map_err(|e| agent_error("create save", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + &state, + &actor, + SAVE_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &record, + "create", + ) + .await?; + Ok(XrpcResponse(CreateSaveOutput { + cid: out.cid, + created: Some(true), + uri: out.uri, + extra_data: None, + })) +} + +pub(super) async fn delete_save( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let uri = match (req.uri, req.subject) { + (Some(uri), _) => uri, + (None, Some(subject)) => { + lookup_social_uri(&state, &actor, SAVE_COLLECTION, subject.uri.as_ref()).await? + } + (None, None) => return Err(invalid_request("deleteSave requires uri or subject")), + }; + delete_social::(&state, &agent, &actor, SAVE_COLLECTION, &uri).await?; + Ok(XrpcResponse(DeleteSaveOutput { + deleted: true, + uri: Some(uri), + extra_data: None, + })) +} + +pub(super) async fn create_tag( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + let _ = ensure_polymodel_profile(&state, &agent, &actor, false).await?; + validate_subject(&state, &req.subject.uri).await?; + let record = Tag { + created_at: Datetime::now(), + subject: req.subject, + tag: req.tag, + extra_data: None, + }; + let out = agent + .create_record(record.clone(), None) + .await + .map_err(|e| agent_error("create tag", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + &state, + &actor, + TAG_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &record, + "create", + ) + .await?; + Ok(XrpcResponse(CreateTagOutput { + cid: out.cid, + created: Some(true), + uri: out.uri, + extra_data: None, + })) +} + +pub(super) async fn delete_tag( + State(state): State, + ExtractOAuthSession(session): ExtractOAuthSession, + ExtractXrpc(req): ExtractXrpc, +) -> AppResult> { + let agent = Agent::from(session); + let actor = authenticated_did(&agent).await?; + delete_social::(&state, &agent, &actor, TAG_COLLECTION, &req.uri).await?; + Ok(XrpcResponse(DeleteTagOutput { + deleted: true, + uri: Some(req.uri), + extra_data: None, + })) +} + +struct BootstrapOutcome { + profile: ProfileView, + status: BootstrapProfileOutputStatus, + uri: Option, + cid: Option, +} + +async fn authenticated_did(agent: &UserAgent) -> AppResult { + agent + .session_info() + .await + .map(|(did, _)| did) + .ok_or_else(|| unauthorized("OAuth session is required")) +} + +async fn ensure_polymodel_profile( + state: &AppState, + agent: &UserAgent, + actor: &Did, + allow_create: bool, +) -> AppResult { + if let Some(profile) = views::polymodel_profile_view(state, actor).await? { + return Ok(BootstrapOutcome { + profile, + status: BootstrapProfileOutputStatus::ExistingLocal, + uri: None, + cid: None, + }); + } + + let profile_uri = at_uri_owned(actor, PROFILE_COLLECTION, "self")?; + match agent.get_record::(&profile_uri).await { + Ok(resp) => { + let output = resp + .into_output() + .map_err(|e| internal(format!("profile record decode failed: {e}")))?; + eager_project_record( + state, + actor, + PROFILE_COLLECTION, + "self", + output.cid.clone(), + &output.value, + "create", + ) + .await?; + let profile = views::polymodel_profile_view(state, actor) + .await? + .ok_or_else(|| internal("projected profile was not readable"))?; + return Ok(BootstrapOutcome { + profile, + status: BootstrapProfileOutputStatus::ExistingRemote, + uri: Some(output.uri), + cid: output.cid, + }); + } + Err(e) if client_error_is_not_found(&e) => {} + Err(e) => return Err(client_error("fetch polymodel profile", e)), + } + + if !allow_create { + return Err(invalid_request( + "Polymodel profile has not been bootstrapped", + )); + } + + let bsky_uri = at_uri_owned(actor, "app.bsky.actor.profile", "self")?; + let bsky = match agent.get_record::(&bsky_uri).await { + Ok(resp) => { + resp.into_output() + .map_err(|e| internal(format!("bsky profile decode failed: {e}")))? + .value + } + Err(e) if client_error_is_not_found(&e) => BskyProfile { + avatar: None, + banner: None, + created_at: None, + description: None, + display_name: None, + joined_via_starter_pack: None, + labels: None, + pinned_post: None, + pronouns: None, + website: None, + extra_data: None, + }, + Err(e) => return Err(client_error("fetch bsky profile", e)), + }; + + let profile = PolymodelProfile { + avatar: bsky.avatar, + default_license: None, + description: bsky.description, + display_name: bsky.display_name, + links: None, + printers: None, + pronouns: bsky.pronouns, + extra_data: None, + }; + let create = agent + .create_record(profile.clone(), Some(record_key("self")?)) + .await; + let output = match create { + Ok(out) => { + eager_project_record( + state, + actor, + PROFILE_COLLECTION, + "self", + Some(out.cid.clone()), + &profile, + "create", + ) + .await?; + BootstrapOutcome { + profile: views::polymodel_profile_view(state, actor) + .await? + .ok_or_else(|| internal("created profile was not readable"))?, + status: BootstrapProfileOutputStatus::Created, + uri: Some(out.uri), + cid: Some(out.cid), + } + } + Err(e) if agent_error_is_conflict(&e) || agent_error_is_not_found(&e) => { + let resp = agent + .get_record::(&profile_uri) + .await + .map_err(|e| client_error("refetch raced profile", e))?; + let remote = resp + .into_output() + .map_err(|e| internal(format!("raced profile decode failed: {e}")))?; + eager_project_record( + state, + actor, + PROFILE_COLLECTION, + "self", + remote.cid.clone(), + &remote.value, + "create", + ) + .await?; + BootstrapOutcome { + profile: views::polymodel_profile_view(state, actor) + .await? + .ok_or_else(|| internal("raced profile was not readable"))?, + status: BootstrapProfileOutputStatus::ExistingRemote, + uri: Some(remote.uri), + cid: remote.cid, + } + } + Err(e) => return Err(agent_error("create polymodel profile", e)), + }; + Ok(output) +} + +async fn publish_composition( + state: &AppState, + agent: &UserAgent, + actor: &Did, + thing: ThingInput, +) -> AppResult { + let mut model_results = Vec::new(); + let mut model_refs = Vec::new(); + + for model_input in thing.models { + let (model_ref, result) = publish_model(state, agent, actor, model_input).await?; + model_refs.push(model_ref); + model_results.push(result); + } + + let record = Thing { + cover: thing.cover, + created_at: Datetime::now(), + derived_from: thing.derived_from, + instructions: thing.instructions, + intended_to_work_with: thing.intended_to_work_with, + license: thing.license, + models: Some(model_refs), + name: thing.name, + previews: thing.previews, + summary: thing.summary, + tags: thing.tags, + extra_data: None, + }; + + let out = agent + .create_record(record.clone(), None) + .await + .map_err(|e| agent_error("create thing", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + state, + actor, + THING_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &record, + "create", + ) + .await?; + let created = RecordRef { + cid: out.cid, + uri: out.uri, + extra_data: None, + }; + + Ok(PublishThingOutput { + thing: created, + models: model_results, + extra_data: None, + }) +} + +async fn update_composition( + state: &AppState, + agent: &UserAgent, + actor: &Did, + thing_uri: AtUri, + thing: UpdateThingInput, + previous: &ExistingComposition, +) -> AppResult { + let mut model_results = Vec::new(); + let mut model_refs = Vec::new(); + + for model_input in thing.models { + let (model_ref, result) = update_model(state, agent, actor, model_input, previous).await?; + model_refs.push(model_ref); + model_results.push(result); + } + + let record = Thing { + cover: thing.cover, + created_at: Datetime::now(), + derived_from: thing.derived_from, + instructions: thing.instructions, + intended_to_work_with: thing.intended_to_work_with, + license: thing.license, + models: Some(model_refs), + name: thing.name, + previews: thing.previews, + summary: thing.summary, + tags: thing.tags, + extra_data: None, + }; + + let rkey = rkey_from_uri(&thing_uri)?; + let out = agent + .put_record::(rkey.clone(), record.clone()) + .await + .map_err(|e| agent_error("put thing", e))?; + eager_project_record( + state, + actor, + THING_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &record, + "update", + ) + .await?; + + Ok(PublishThingOutput { + thing: RecordRef { + cid: out.cid, + uri: out.uri, + extra_data: None, + }, + models: model_results, + extra_data: None, + }) +} + +async fn update_model( + state: &AppState, + agent: &UserAgent, + actor: &Did, + model_input: UpdateModelInput, + previous: &ExistingComposition, +) -> AppResult<(StrongRef, ModelPublishResult)> { + let mut part_refs = Vec::new(); + let mut part_record_refs = Vec::new(); + for part_input in model_input.parts { + let part_ref = update_part(state, agent, actor, part_input, previous).await?; + part_record_refs.push(RecordRef { + uri: part_ref.uri.clone(), + cid: part_ref.cid.clone(), + extra_data: None, + }); + part_refs.push(part_ref); + } + + let model = Model { + cover: model_input.cover, + created_at: Datetime::now(), + derived_from: model_input.derived_from, + instructions: model_input.instructions, + intended_to_work_with: model_input.intended_to_work_with, + license: model_input.license, + name: model_input.name, + parts: part_refs, + previews: model_input.previews, + summary: model_input.summary, + tags: model_input.tags, + extra_data: None, + }; + + let (uri, cid, action) = if let Some(uri) = model_input.uri { + validate_owned_uri(actor, &uri, MODEL_COLLECTION)?; + let rkey = rkey_from_uri(&uri)?; + validate_existing_update_child( + state, + actor, + &uri, + &rkey, + MODEL_COLLECTION, + &previous.models, + ) + .await?; + let out = agent + .put_record::(rkey.clone(), model.clone()) + .await + .map_err(|e| agent_error("put model", e))?; + eager_project_record( + state, + actor, + MODEL_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &model, + "update", + ) + .await?; + (out.uri, out.cid, "update") + } else { + let out = agent + .create_record(model.clone(), None) + .await + .map_err(|e| agent_error("create model", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + state, + actor, + MODEL_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &model, + "create", + ) + .await?; + (out.uri, out.cid, "create") + }; + tracing::debug!(action, uri = %uri, "updated thing model composition member"); + let model_ref = strong_ref(uri.clone(), cid.clone()); + Ok(( + model_ref, + ModelPublishResult { + model: RecordRef { + uri, + cid, + extra_data: None, + }, + parts: part_record_refs, + extra_data: None, + }, + )) +} + +async fn update_part( + state: &AppState, + agent: &UserAgent, + actor: &Did, + input: UpdatePartInput, + previous: &ExistingComposition, +) -> AppResult { + let file = match (input.file, input.upload_id) { + (Some(file), _) => file, + (None, Some(upload_id)) => load_upload_file(state, actor, upload_id.as_ref()).await?, + (None, None) => return Err(invalid_request("part requires file or uploadId")), + }; + let part = Part { + created_at: Datetime::now(), + derived_from: input.derived_from, + dimensions: input.dimensions, + file, + format: input.format, + intended_to_work_with: input.intended_to_work_with, + name: input.name, + notes: input.notes, + previews: input.previews, + print_settings: input.print_settings, + units: input.units, + extra_data: None, + }; + + if let Some(uri) = input.uri { + validate_owned_uri(actor, &uri, PART_COLLECTION)?; + let rkey = rkey_from_uri(&uri)?; + validate_existing_update_child(state, actor, &uri, &rkey, PART_COLLECTION, &previous.parts) + .await?; + let out = agent + .put_record::(rkey.clone(), part.clone()) + .await + .map_err(|e| agent_error("put part", e))?; + eager_project_record( + state, + actor, + PART_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &part, + "update", + ) + .await?; + return Ok(strong_ref(out.uri, out.cid)); + } + + let out = agent + .create_record(part.clone(), None) + .await + .map_err(|e| agent_error("create part", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + state, + actor, + PART_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &part, + "create", + ) + .await?; + Ok(strong_ref(out.uri, out.cid)) +} + +async fn publish_model( + state: &AppState, + agent: &UserAgent, + actor: &Did, + model_input: ModelInput, +) -> AppResult<(StrongRef, ModelPublishResult)> { + let mut part_refs = Vec::new(); + let mut part_record_refs = Vec::new(); + for part_input in model_input.parts { + let part_ref = publish_part(state, agent, actor, part_input).await?; + part_record_refs.push(RecordRef { + uri: part_ref.uri.clone(), + cid: part_ref.cid.clone(), + extra_data: None, + }); + part_refs.push(part_ref); + } + let model = Model { + cover: model_input.cover, + created_at: Datetime::now(), + derived_from: model_input.derived_from, + instructions: model_input.instructions, + intended_to_work_with: model_input.intended_to_work_with, + license: model_input.license, + name: model_input.name, + parts: part_refs, + previews: model_input.previews, + summary: model_input.summary, + tags: model_input.tags, + extra_data: None, + }; + let out = agent + .create_record(model.clone(), None) + .await + .map_err(|e| agent_error("create model", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + state, + actor, + MODEL_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &model, + "create", + ) + .await?; + let model_ref = strong_ref(out.uri.clone(), out.cid.clone()); + let record_ref = RecordRef { + uri: out.uri, + cid: out.cid, + extra_data: None, + }; + Ok(( + model_ref, + ModelPublishResult { + model: record_ref, + parts: part_record_refs, + extra_data: None, + }, + )) +} + +async fn publish_part( + state: &AppState, + agent: &UserAgent, + actor: &Did, + input: polymodel_api::space_polymodel::library::publish_thing::PartInput, +) -> AppResult { + let file = match (input.file, input.upload_id) { + (Some(file), _) => file, + (None, Some(upload_id)) => load_upload_file(state, actor, upload_id.as_ref()).await?, + (None, None) => return Err(invalid_request("part requires file or uploadId")), + }; + let part = Part { + created_at: Datetime::now(), + derived_from: input.derived_from, + dimensions: input.dimensions, + file, + format: input.format, + intended_to_work_with: input.intended_to_work_with, + name: input.name, + notes: input.notes, + previews: input.previews, + print_settings: input.print_settings, + units: input.units, + extra_data: None, + }; + let out = agent + .create_record(part.clone(), None) + .await + .map_err(|e| agent_error("create part", e))?; + let rkey = rkey_from_uri(&out.uri)?; + eager_project_record( + state, + actor, + PART_COLLECTION, + rkey.as_ref(), + Some(out.cid.clone()), + &part, + "create", + ) + .await?; + Ok(strong_ref(out.uri, out.cid)) +} + +async fn delete_social( + state: &AppState, + agent: &UserAgent, + actor: &Did, + collection: &str, + uri: &AtUri, +) -> AppResult<()> +where + R: Collection + Serialize, +{ + validate_owned_uri(actor, uri, collection)?; + let rkey = rkey_from_uri(uri)?; + agent_delete_record::(agent, rkey.clone()).await?; + eager_delete(state, actor, collection, rkey.as_ref()).await +} + +struct ExistingComposition { + thing_uri: AtUri, + thing_rkey: RecordKey, + models: Vec, + parts: Vec, +} + +struct CompositionChild { + rkey: RecordKey, + uri: AtUri, +} + +#[derive(sqlx::FromRow)] +struct CompositionRecordRow { + did: String, + rkey: String, + uri: String, +} + +async fn load_owned_composition( + state: &AppState, + actor: &Did, + thing_uri: &AtUri, +) -> AppResult { + let thing = db(sqlx::query_as!( + CompositionRecordRow, + "SELECT did, rkey, uri FROM things WHERE uri = ?", + thing_uri.as_ref() + ) + .fetch_optional(&state.pool) + .await)? + .ok_or_else(|| invalid_request("thing is not indexed locally"))?; + ensure_row_owned(actor, &thing, THING_COLLECTION)?; + + let models = db(sqlx::query_as!( + CompositionRecordRow, + "SELECT m.did, m.rkey, m.uri FROM thing_models tm \ + JOIN models m ON m.uri = tm.model_uri \ + WHERE tm.thing_uri = ? ORDER BY tm.position", + thing_uri.as_ref() + ) + .fetch_all(&state.pool) + .await)?; + let parts = db(sqlx::query_as!( + CompositionRecordRow, + "SELECT p.did, p.rkey, p.uri FROM thing_models tm \ + JOIN model_parts mp ON mp.model_uri = tm.model_uri \ + JOIN parts p ON p.uri = mp.part_uri \ + WHERE tm.thing_uri = ? ORDER BY tm.position, mp.position", + thing_uri.as_ref() + ) + .fetch_all(&state.pool) + .await)?; + + let thing_uri = at_uri_from_row(&thing)?; + let mut model_children = Vec::new(); + let mut seen_models = BTreeSet::new(); + for row in models { + ensure_row_owned(actor, &row, MODEL_COLLECTION)?; + if seen_models.insert(row.uri.clone()) { + model_children.push(CompositionChild { + rkey: record_key(&row.rkey)?, + uri: at_uri_from_row(&row)?, + }); + } + } + let mut part_children = Vec::new(); + let mut seen_parts = BTreeSet::new(); + for row in parts { + ensure_row_owned(actor, &row, PART_COLLECTION)?; + if seen_parts.insert(row.uri.clone()) { + part_children.push(CompositionChild { + rkey: record_key(&row.rkey)?, + uri: at_uri_from_row(&row)?, + }); + } + } + + Ok(ExistingComposition { + thing_uri, + thing_rkey: record_key(&thing.rkey)?, + models: model_children, + parts: part_children, + }) +} + +async fn delete_composition_children( + state: &AppState, + agent: &UserAgent, + actor: &Did, + composition: &ExistingComposition, +) -> AppResult> { + let mut deleted_uris = Vec::new(); + for model in &composition.models { + if model_reference_count(state, &model.uri).await? > 1 { + continue; + } + agent_delete_record::(agent, model.rkey.clone()).await?; + eager_delete(state, actor, MODEL_COLLECTION, model.rkey.as_ref()).await?; + deleted_uris.push(model.uri.clone()); + } + for part in &composition.parts { + if part_reference_count(state, &part.uri).await? > 0 { + continue; + } + agent_delete_record::(agent, part.rkey.clone()).await?; + eager_delete(state, actor, PART_COLLECTION, part.rkey.as_ref()).await?; + deleted_uris.push(part.uri.clone()); + } + Ok(deleted_uris) +} + +async fn model_reference_count(state: &AppState, uri: &AtUri) -> AppResult { + db(sqlx::query_scalar!( + "SELECT COUNT(*) AS count FROM thing_models WHERE model_uri = ?", + uri.as_ref() + ) + .fetch_one(&state.pool) + .await) +} + +async fn part_reference_count(state: &AppState, uri: &AtUri) -> AppResult { + db(sqlx::query_scalar!( + "SELECT COUNT(*) AS count FROM model_parts WHERE part_uri = ?", + uri.as_ref() + ) + .fetch_one(&state.pool) + .await) +} + +fn ensure_row_owned(actor: &Did, row: &impl OwnedRow, collection: &str) -> AppResult<()> { + if row.did() != actor.as_ref() { + return Err(invalid_request(format!( + "{collection} composition record is not owned by the authenticated actor" + ))); + } + Ok(()) +} + +trait OwnedRow { + fn did(&self) -> &str; + fn uri(&self) -> &str; +} + +impl OwnedRow for CompositionRecordRow { + fn did(&self) -> &str { + &self.did + } + fn uri(&self) -> &str { + &self.uri + } +} + +fn at_uri_from_row(row: &impl OwnedRow) -> AppResult { + AtUri::new_owned(row.uri()).map_err(|e| internal(format!("stored at-uri is invalid: {e}"))) +} + +fn strong_ref(uri: AtUri, cid: Cid) -> StrongRef { + StrongRef { + uri, + cid, + extra_data: None, + } +} + +fn record_key(value: &str) -> AppResult> { + RecordKey::any(SmolStr::from(value)) + .map_err(|e| invalid_request(format!("invalid rkey {value:?}: {e}"))) +} + +fn rkey_from_uri(uri: &AtUri) -> AppResult> { + let rkey = uri + .rkey() + .ok_or_else(|| invalid_request("at-uri is missing record key"))?; + record_key(rkey.as_ref()) +} + +fn at_uri_owned(actor: &Did, collection: &str, rkey: &str) -> AppResult { + AtUri::new_owned(format!("at://{}/{}/{}", actor.as_ref(), collection, rkey)) + .map_err(|e| invalid_request(format!("invalid at-uri: {e}"))) +} + +fn validate_owned_uri(actor: &Did, uri: &AtUri, expected_collection: &str) -> AppResult<()> { + match uri.authority() { + AtIdentifier::Did(did) if did.as_ref() == actor.as_ref() => {} + _ => { + return Err(invalid_request( + "record URI is not owned by the authenticated actor", + )); + } + } + let collection = uri + .collection() + .ok_or_else(|| invalid_request("record URI missing collection"))?; + if collection.as_ref() != expected_collection { + return Err(invalid_request(format!( + "expected {expected_collection} URI" + ))); + } + if uri.rkey().is_none() { + return Err(invalid_request("record URI missing rkey")); + } + Ok(()) +} + +async fn validate_existing_update_child( + state: &AppState, + actor: &Did, + uri: &AtUri, + rkey: &RecordKey, + collection: &str, + current_children: &[CompositionChild], +) -> AppResult<()> { + if current_children.iter().any(|child| &child.rkey == rkey) { + return Ok(()); + } + let exists = match collection { + MODEL_COLLECTION => db(sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM models WHERE did = ? AND uri = ?)", + actor.as_ref(), + uri.as_ref() + ) + .fetch_one(&state.pool) + .await)?, + PART_COLLECTION => db(sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM parts WHERE did = ? AND uri = ?)", + actor.as_ref(), + uri.as_ref() + ) + .fetch_one(&state.pool) + .await)?, + _ => return Err(invalid_request("unsupported composition child collection")), + }; + if exists == 0 { + return Err(invalid_request( + "existing child uri is not indexed as an owned composition record", + )); + } + Ok(()) +} + +async fn validate_subject(state: &AppState, uri: &AtUri) -> AppResult<()> { + let collection = uri + .collection() + .ok_or_else(|| invalid_request("subject URI missing collection"))?; + let table = match collection.as_ref() { + THING_COLLECTION => "things", + MODEL_COLLECTION => "models", + PART_COLLECTION => "parts", + _ => { + return Err(invalid_request( + "subject must be a Polymodel thing, model, or part", + )); + } + }; + let exists = match table { + "things" => db(sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM things WHERE uri = ?)", + uri.as_ref() + ) + .fetch_one(&state.pool) + .await)?, + "models" => db(sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM models WHERE uri = ?)", + uri.as_ref() + ) + .fetch_one(&state.pool) + .await)?, + "parts" => db(sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM parts WHERE uri = ?)", + uri.as_ref() + ) + .fetch_one(&state.pool) + .await)?, + _ => unreachable!(), + }; + if exists == 0 { + return Err(invalid_request("subject is not indexed locally")); + } + Ok(()) +} + +async fn eager_project_record( + state: &AppState, + actor: &Did, + collection: &str, + rkey: &str, + cid: Option, + record: &R, + action: &str, +) -> AppResult<()> { + let record = + serde_json::to_value(record).map_err(|e| internal(format!("record serialize: {e}")))?; + let event = ProjectionEvent { + seq: 0, + did: actor.as_ref().to_string(), + collection: collection.to_string(), + rkey: rkey.to_string(), + action: action.to_string(), + record: Some(record), + cid: cid.map(|c| c.to_string()), + }; + project_eager_record(&state.pool, &event) + .await + .map_err(|e| internal(format!("eager projection failed: {e}"))) +} + +async fn eager_delete( + state: &AppState, + actor: &Did, + collection: &str, + rkey: &str, +) -> AppResult<()> { + let event = ProjectionEvent { + seq: 0, + did: actor.as_ref().to_string(), + collection: collection.to_string(), + rkey: rkey.to_string(), + action: "delete".to_string(), + record: None, + cid: None, + }; + project_eager_record(&state.pool, &event) + .await + .map_err(|e| internal(format!("eager delete projection failed: {e}"))) +} + +struct StreamedUpload { + chunks: Vec, + digest: String, + size: usize, +} + +async fn upload_streamed_file( + agent: &UserAgent, + mut body: Body, + mime_type: &str, +) -> AppResult { + let mut hasher = Sha256::new(); + let mut chunks = Vec::new(); + let mut pending = Vec::with_capacity(CHUNK_SIZE); + let mut size = 0usize; + + while let Some(frame) = body + .frame() + .await + .transpose() + .map_err(|e| invalid_request(format!("stageFile body read failed: {e}")))? + { + let Ok(data) = frame.into_data() else { + continue; + }; + size = size + .checked_add(data.len()) + .ok_or_else(|| invalid_request("stageFile body is too large"))?; + if size > MAX_STAGE_FILE_SIZE { + return Err(invalid_request(format!( + "stageFile body exceeds the {} byte limit", + MAX_STAGE_FILE_SIZE + ))); + } + hasher.update(&data); + let mut remaining = data.as_ref(); + while !remaining.is_empty() { + let take = (CHUNK_SIZE - pending.len()).min(remaining.len()); + pending.extend_from_slice(&remaining[..take]); + remaining = &remaining[take..]; + if pending.len() == CHUNK_SIZE { + let offset = size - remaining.len() - pending.len(); + upload_one_chunk( + agent, + mime_type, + &mut chunks, + offset, + std::mem::take(&mut pending), + ) + .await?; + pending = Vec::with_capacity(CHUNK_SIZE); + } + } + } + + if size == 0 { + return Err(invalid_request( + "stageFile requires a non-empty raw byte body", + )); + } + if !pending.is_empty() { + let offset = size - pending.len(); + upload_one_chunk(agent, mime_type, &mut chunks, offset, pending).await?; + } + let digest = hasher + .finalize() + .iter() + .map(|b| format!("{b:02x}")) + .collect(); + Ok(StreamedUpload { + chunks, + digest, + size, + }) +} + +async fn upload_one_chunk( + agent: &UserAgent, + mime_type: &str, + chunks: &mut Vec, + offset: usize, + bytes: Vec, +) -> AppResult<()> { + let size = bytes.len(); + let blob = agent_upload_blob(agent, bytes, mime_type).await?; + chunks.push(Chunk { + blob: BlobRef::from(blob), + offset: offset as i64, + size: size as i64, + extra_data: None, + }); + Ok(()) +} + +async fn agent_upload_blob( + agent: &UserAgent, + bytes: Vec, + mime_type: &str, +) -> AppResult { + agent + .upload_blob(bytes, MimeType::new(mime_type)) + .await + .map_err(|e| agent_error("upload blob", e)) +} + +async fn agent_delete_record(agent: &UserAgent, rkey: RecordKey) -> AppResult<()> +where + R: Collection + Serialize, +{ + agent + .delete_record::(rkey) + .await + .map(|_| ()) + .map_err(|e| agent_error("delete record", e)) +} + +struct UploadPersistence<'a> { + upload_id: &'a str, + digest: &'a str, + mime_type: &'a str, + size: usize, + filename: Option<&'a str>, + file: &'a File, +} + +async fn persist_upload( + state: &AppState, + actor: &Did, + upload: UploadPersistence<'_>, +) -> AppResult<()> { + let file_json = serde_json::to_string(upload.file) + .map_err(|e| internal(format!("file metadata serialize: {e}")))?; + let chunks_json = serde_json::to_string(&upload.file.chunks) + .map_err(|e| internal(format!("chunk metadata serialize: {e}")))?; + let now = chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default(); + db(sqlx::query!( + "INSERT INTO upload_staging (owner_did, upload_id, sha256, mime_type, size, filename, status, file_json, chunks_json, created_at, updated_at) \ + VALUES (?, ?, ?, ?, ?, ?, 'uploaded', ?, ?, ?, ?) \ + ON CONFLICT(owner_did, upload_id) DO UPDATE SET sha256 = excluded.sha256, mime_type = excluded.mime_type, size = excluded.size, filename = excluded.filename, status = excluded.status, file_json = excluded.file_json, chunks_json = excluded.chunks_json, updated_at = excluded.updated_at", + actor.as_ref(), + upload.upload_id, + upload.digest, + upload.mime_type, + upload.size as i64, + upload.filename, + file_json, + chunks_json, + now, + now, + ).execute(&state.pool).await)?; + Ok(()) +} + +async fn load_upload_file(state: &AppState, actor: &Did, upload_id: &str) -> AppResult { + #[derive(sqlx::FromRow)] + struct Row { + file_json: String, + } + let row = db(sqlx::query_as!(Row, "SELECT file_json FROM upload_staging WHERE owner_did = ? AND upload_id = ? AND status = 'uploaded'", actor.as_ref(), upload_id).fetch_optional(&state.pool).await)? + .ok_or_else(|| invalid_request("unknown or unavailable uploadId"))?; + serde_json::from_str(&row.file_json) + .map_err(|e| internal(format!("staged file metadata parse: {e}"))) +} + +async fn lookup_social_uri( + state: &AppState, + actor: &Did, + collection: &str, + subject_uri: &str, +) -> AppResult { + lookup_existing_social(state, actor, collection, subject_uri) + .await? + .map(|record| record.uri) + .ok_or_else(|| invalid_request("social record is not indexed locally")) +} + +struct ExistingSocial { + cid: Cid, + uri: AtUri, +} + +async fn lookup_existing_social( + state: &AppState, + actor: &Did, + collection: &str, + subject_uri: &str, +) -> AppResult> { + #[derive(sqlx::FromRow)] + struct Row { + cid: String, + rkey: String, + } + let row = match collection { + LIKE_COLLECTION => db(sqlx::query_as!( + Row, + "SELECT cid, rkey FROM likes WHERE did = ? AND subject_uri = ?", + actor.as_ref(), + subject_uri + ) + .fetch_optional(&state.pool) + .await)?, + SAVE_COLLECTION => db(sqlx::query_as!( + Row, + "SELECT cid, rkey FROM saves WHERE did = ? AND subject_uri = ?", + actor.as_ref(), + subject_uri + ) + .fetch_optional(&state.pool) + .await)?, + _ => return Err(invalid_request("unsupported social lookup")), + }; + let existing = row + .map(|row| { + let uri = at_uri_owned(actor, collection, &row.rkey)?; + let cid = Cid::new_owned(row.cid.as_bytes()) + .map_err(|e| internal(format!("stored social cid is invalid: {e}")))?; + Ok(ExistingSocial { cid, uri }) + }) + .transpose()?; + Ok(existing) +} + +fn content_type(headers: &HeaderMap) -> String { + headers + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.split(';').next()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .unwrap_or("application/octet-stream") + .to_string() +} + +fn filename_header(headers: &HeaderMap) -> Option { + headers + .get("x-polymodel-filename") + .and_then(|v| v.to_str().ok()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(ToOwned::to_owned) +} + +#[cfg(test)] +fn hex_sha256(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + digest.iter().map(|b| format!("{b:02x}")).collect() +} + +fn agent_error(operation: &str, err: jacquard::client::AgentError) -> AppError { + if agent_error_is_auth(&err) { + tracing::warn!(operation, error = %err, "authenticated PDS operation failed authorization"); + return unauthorized("authenticated PDS operation was not authorized"); + } + tracing::error!(operation, error = %err, "authenticated PDS operation failed"); + internal("authenticated PDS operation failed") +} + +fn client_error(operation: &str, err: jacquard_common::error::ClientError) -> AppError { + let text = format!("{err:?}"); + if text.contains("Auth") || text.contains("Unauthorized") || text.contains("401") { + tracing::warn!(operation, error = %err, "authenticated PDS read failed authorization"); + return unauthorized("authenticated PDS read was not authorized"); + } + tracing::error!(operation, error = %err, "authenticated PDS read failed"); + internal("authenticated PDS read failed") +} + +fn client_error_is_not_found(err: &jacquard_common::error::ClientError) -> bool { + let text = format!("{err:?}"); + text.contains("RecordNotFound") || text.contains("not found") || text.contains("404") +} + +fn agent_error_is_auth(err: &jacquard::client::AgentError) -> bool { + format!("{err:?}").contains("Auth") +} + +fn agent_error_is_not_found(err: &jacquard::client::AgentError) -> bool { + let text = format!("{err:?}"); + text.contains("RecordNotFound") || text.contains("not found") || text.contains("404") +} + +fn agent_error_is_conflict(err: &jacquard::client::AgentError) -> bool { + let text = format!("{err:?}"); + text.contains("AlreadyExists") || text.contains("InvalidSwap") || text.contains("409") +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use jacquard_common::types::string::Did; + use serde_json::json; + use sqlx::SqlitePool; + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; + + use super::*; + + const DID_A: &str = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa"; + const DID_B: &str = "did:plc:bbbbbbbbbbbbbbbbbbbbbbbb"; + + async fn state() -> AppState { + let options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await + .unwrap(); + sqlx::migrate!("./migrations").run(&pool).await.unwrap(); + let bootstrap = crate::oauth::bootstrap_oauth(pool.clone(), Some("http://localhost")) + .expect("ephemeral OAuth bootstrap for tests"); + AppState::new(pool, bootstrap) + } + + fn did(s: &str) -> Did { + Did::new_owned(s).unwrap() + } + + fn uri(did: &str, collection: &str, rkey: &str) -> String { + format!("at://{did}/{collection}/{rkey}") + } + + async fn seed_content_row( + pool: &SqlitePool, + table: &str, + did: &str, + rkey: &str, + name: &str, + ) -> String { + let collection = match table { + "things" => THING_COLLECTION, + "models" => MODEL_COLLECTION, + "parts" => PART_COLLECTION, + _ => unreachable!(), + }; + let uri = uri(did, collection, rkey); + let now = chrono::Utc::now().timestamp_nanos_opt().unwrap(); + match table { + "things" => { + sqlx::query( + "INSERT INTO things (did, rkey, uri, cid, name, summary, license, tags_json, tags_text, instructions_text, cover_json, derived_from_uri, created_at, indexed_at, record_json) VALUES (?, ?, ?, 'bafycid', ?, NULL, 'CC-BY-4.0', NULL, NULL, NULL, NULL, NULL, ?, ?, ?)", + ) + .bind(did) + .bind(rkey) + .bind(&uri) + .bind(name) + .bind(now) + .bind(now) + .bind(json!({"name": name, "license": "CC-BY-4.0", "models": []}).to_string()) + .execute(pool) + .await + .unwrap(); + } + "models" => { + sqlx::query( + "INSERT INTO models (did, rkey, uri, cid, name, summary, created_at, indexed_at, record_json) VALUES (?, ?, ?, 'bafycid', ?, NULL, ?, ?, ?)", + ) + .bind(did) + .bind(rkey) + .bind(&uri) + .bind(name) + .bind(now) + .bind(now) + .bind(json!({"name": name, "parts": []}).to_string()) + .execute(pool) + .await + .unwrap(); + } + "parts" => { + let file_json = + json!({"mimeType": "model/stl", "size": 1, "chunks": []}).to_string(); + sqlx::query( + "INSERT INTO parts (did, rkey, uri, cid, name, format, file_json, created_at, indexed_at, record_json) VALUES (?, ?, ?, 'bafycid', ?, 'stl', ?, ?, ?, ?)", + ) + .bind(did) + .bind(rkey) + .bind(&uri) + .bind(name) + .bind(&file_json) + .bind(now) + .bind(now) + .bind(json!({"name": name, "file": {"mimeType": "model/stl", "size": 1, "chunks": []}}).to_string()) + .execute(pool) + .await + .unwrap(); + } + _ => unreachable!(), + } + uri + } + + #[tokio::test] + async fn upload_staging_round_trips_owner_scoped_file_metadata() { + let state = state().await; + let actor = did(DID_A); + let bytes = b"solid cube"; + let digest = hex_sha256(bytes); + let file = File { + chunks: Vec::new(), + digest: Some(SmolStr::from(digest.clone())), + mime_type: SmolStr::from("model/stl"), + size: bytes.len() as i64, + extra_data: None, + }; + + persist_upload( + &state, + &actor, + UploadPersistence { + upload_id: "upload-1", + digest: &digest, + mime_type: "model/stl", + size: bytes.len(), + filename: Some("cube.stl"), + file: &file, + }, + ) + .await + .unwrap(); + + let loaded = load_upload_file(&state, &actor, "upload-1").await.unwrap(); + assert_eq!(loaded.digest.as_deref(), Some(digest.as_str())); + assert_eq!(loaded.mime_type.as_str(), "model/stl"); + assert_eq!(loaded.size, bytes.len() as i64); + assert!( + load_upload_file(&state, &did(DID_B), "upload-1") + .await + .is_err() + ); + } + + #[tokio::test] + async fn existing_like_lookup_returns_uri_and_cid_for_idempotent_duplicate_create() { + let state = state().await; + let actor = did(DID_A); + let subject = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing").await; + let cid = "bafyreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + sqlx::query( + "INSERT INTO likes (did, rkey, cid, subject_uri, created_at) VALUES (?, ?, ?, ?, 1)", + ) + .bind(DID_A) + .bind("like1") + .bind(cid) + .bind(&subject) + .execute(&state.pool) + .await + .unwrap(); + + let existing = lookup_existing_social(&state, &actor, LIKE_COLLECTION, &subject) + .await + .unwrap() + .expect("like should be indexed"); + assert_eq!(existing.uri.as_ref(), uri(DID_A, LIKE_COLLECTION, "like1")); + assert_eq!(existing.cid.to_string(), cid); + } + + #[tokio::test] + async fn subject_validation_accepts_indexed_content_and_rejects_unknowns() { + let state = state().await; + let thing = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing").await; + let thing_uri = AtUri::new_owned(&thing).unwrap(); + validate_subject(&state, &thing_uri).await.unwrap(); + + let missing = AtUri::new_owned(uri(DID_A, THING_COLLECTION, "missing")).unwrap(); + assert!(validate_subject(&state, &missing).await.is_err()); + let unsupported = AtUri::new_owned(uri(DID_A, LIKE_COLLECTION, "like1")).unwrap(); + assert!(validate_subject(&state, &unsupported).await.is_err()); + } + + #[tokio::test] + async fn composition_load_requires_all_existing_children_to_be_owned() { + let state = state().await; + let thing = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing").await; + let model = seed_content_row(&state.pool, "models", DID_A, "model1", "Model").await; + let part = seed_content_row(&state.pool, "parts", DID_A, "part1", "Part").await; + sqlx::query("INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, 0)") + .bind(&thing) + .bind(&model) + .execute(&state.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, 0)") + .bind(&model) + .bind(&part) + .execute(&state.pool) + .await + .unwrap(); + + let composition = + load_owned_composition(&state, &did(DID_A), &AtUri::new_owned(&thing).unwrap()) + .await + .unwrap(); + assert_eq!(composition.models.len(), 1); + assert_eq!(composition.parts.len(), 1); + + sqlx::query("UPDATE parts SET did = ? WHERE uri = ?") + .bind(DID_B) + .bind(&part) + .execute(&state.pool) + .await + .unwrap(); + assert!( + load_owned_composition(&state, &did(DID_A), &AtUri::new_owned(&thing).unwrap()) + .await + .is_err() + ); + } + + #[tokio::test] + async fn update_child_validation_allows_owned_detached_records_to_be_reattached() { + let state = state().await; + let actor = did(DID_A); + let thing = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing").await; + let active_model = seed_content_row(&state.pool, "models", DID_A, "model1", "Model").await; + let detached_model = + seed_content_row(&state.pool, "models", DID_A, "model2", "Detached").await; + let foreign_model = + seed_content_row(&state.pool, "models", DID_B, "model3", "Foreign").await; + sqlx::query("INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, 0)") + .bind(&thing) + .bind(&active_model) + .execute(&state.pool) + .await + .unwrap(); + + let previous = load_owned_composition(&state, &actor, &AtUri::new_owned(&thing).unwrap()) + .await + .unwrap(); + let detached_uri = AtUri::new_owned(&detached_model).unwrap(); + let detached_rkey = rkey_from_uri(&detached_uri).unwrap(); + validate_existing_update_child( + &state, + &actor, + &detached_uri, + &detached_rkey, + MODEL_COLLECTION, + &previous.models, + ) + .await + .unwrap(); + + let foreign_uri = AtUri::new_owned(&foreign_model).unwrap(); + let foreign_rkey = rkey_from_uri(&foreign_uri).unwrap(); + assert!( + validate_existing_update_child( + &state, + &actor, + &foreign_uri, + &foreign_rkey, + MODEL_COLLECTION, + &previous.models, + ) + .await + .is_err() + ); + } + + #[tokio::test] + async fn child_reference_counts_identify_shared_delete_children() { + let state = state().await; + let thing_a = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing A").await; + let thing_b = seed_content_row(&state.pool, "things", DID_A, "thing2", "Thing B").await; + let model = seed_content_row(&state.pool, "models", DID_A, "model1", "Model").await; + let part = seed_content_row(&state.pool, "parts", DID_A, "part1", "Part").await; + sqlx::query("INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, 0)") + .bind(&thing_a) + .bind(&model) + .execute(&state.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, 0)") + .bind(&thing_b) + .bind(&model) + .execute(&state.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, 0)") + .bind(&model) + .bind(&part) + .execute(&state.pool) + .await + .unwrap(); + + assert_eq!( + model_reference_count(&state, &AtUri::new_owned(&model).unwrap()) + .await + .unwrap(), + 2 + ); + assert_eq!( + part_reference_count(&state, &AtUri::new_owned(&part).unwrap()) + .await + .unwrap(), + 1 + ); + } + + #[tokio::test] + async fn child_reference_counts_preserve_parts_reused_across_models() { + let state = state().await; + let thing = seed_content_row(&state.pool, "things", DID_A, "thing1", "Thing").await; + let model_a = seed_content_row(&state.pool, "models", DID_A, "model1", "Model A").await; + let model_b = seed_content_row(&state.pool, "models", DID_A, "model2", "Model B").await; + let part = seed_content_row(&state.pool, "parts", DID_A, "part1", "Shared Part").await; + for (position, model) in [(0, &model_a), (1, &model_b)] { + sqlx::query( + "INSERT INTO thing_models (thing_uri, model_uri, position) VALUES (?, ?, ?)", + ) + .bind(&thing) + .bind(model) + .bind(position) + .execute(&state.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO model_parts (model_uri, part_uri, position) VALUES (?, ?, 0)") + .bind(model) + .bind(&part) + .execute(&state.pool) + .await + .unwrap(); + } + + let composition = + load_owned_composition(&state, &did(DID_A), &AtUri::new_owned(&thing).unwrap()) + .await + .unwrap(); + assert_eq!(composition.models.len(), 2); + assert_eq!( + composition.parts.len(), + 1, + "reused part should be deduped once" + ); + assert_eq!( + part_reference_count(&state, &AtUri::new_owned(&part).unwrap()) + .await + .unwrap(), + 2 + ); + } +} diff --git a/src/indexing/projection.rs b/src/indexing/projection.rs index 0fb6d11..7d57b8b 100644 --- a/src/indexing/projection.rs +++ b/src/indexing/projection.rs @@ -107,6 +107,20 @@ pub async fn project_event(db: &SqlitePool, input: &ProjectionInput) -> anyhow:: Ok(()) } +/// Eagerly project a locally confirmed write without advancing the Hydrant +/// cursor. +/// +/// PM-28 write handlers call this only after the user's PDS has accepted the +/// record/blob operation. Hydrant/firehose will later deliver the same record +/// event; sharing [`project_record`] keeps that convergence idempotent and avoids +/// a subtly different SQL path for read-your-own-writes. +pub async fn project_eager_record(db: &SqlitePool, event: &ProjectionEvent) -> anyhow::Result<()> { + let mut tx = db.begin().await?; + project_record(&mut tx, event).await?; + tx.commit().await?; + Ok(()) +} + /// Dispatch a record event to its collection-specific projection rule. async fn project_record( tx: &mut Transaction<'_, Sqlite>, @@ -600,12 +614,16 @@ async fn project_like_create( let Some(subject) = ref_uri(rec, "subject") else { return Ok(()); }; + let Some(cid) = e.cid.clone() else { + return Ok(()); + }; let created_at = parse_nanos(rec, "createdAt").unwrap_or_else(now_nanos); let res = sqlx::query!( - "INSERT OR IGNORE INTO likes (did, rkey, subject_uri, created_at) VALUES (?, ?, ?, ?)", + "INSERT OR IGNORE INTO likes (did, rkey, cid, subject_uri, created_at) VALUES (?, ?, ?, ?, ?)", e.did, e.rkey, + cid, subject, created_at, ) @@ -668,14 +686,18 @@ async fn project_save_create( let Some(subject) = ref_uri(rec, "subject") else { return Ok(()); }; + let Some(cid) = e.cid.clone() else { + return Ok(()); + }; let note = s_str(rec, "note"); let created_at = parse_nanos(rec, "createdAt").unwrap_or_else(now_nanos); let res = sqlx::query!( - "INSERT OR IGNORE INTO saves (did, rkey, subject_uri, note, created_at) \ - VALUES (?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO saves (did, rkey, cid, subject_uri, note, created_at) \ + VALUES (?, ?, ?, ?, ?, ?)", e.did, e.rkey, + cid, subject, note, created_at, @@ -742,13 +764,17 @@ async fn project_tag_create( let Some(tag) = s_str(rec, "tag") else { return Ok(()); }; + let Some(cid) = e.cid.clone() else { + return Ok(()); + }; let created_at = parse_nanos(rec, "createdAt").unwrap_or_else(now_nanos); let res = sqlx::query!( - "INSERT OR IGNORE INTO tags (did, rkey, subject_uri, tag, created_at) \ - VALUES (?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO tags (did, rkey, cid, subject_uri, tag, created_at) \ + VALUES (?, ?, ?, ?, ?, ?)", e.did, e.rkey, + cid, subject, tag, created_at, @@ -811,14 +837,18 @@ async fn project_listitem_create( let Some(subject_uri) = ref_uri(rec, "subject") else { return Ok(()); }; + let Some(cid) = e.cid.clone() else { + return Ok(()); + }; let created_at = parse_nanos(rec, "createdAt").unwrap_or_else(now_nanos); // No denormalized count for listitems; list membership is queried directly. sqlx::query!( - "INSERT OR IGNORE INTO listitems (did, rkey, list_uri, subject_uri, created_at) \ - VALUES (?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO listitems (did, rkey, cid, list_uri, subject_uri, created_at) \ + VALUES (?, ?, ?, ?, ?, ?)", e.did, e.rkey, + cid, list_uri, subject_uri, created_at, -- 2.51.2