diff --git a/lexicons/dev/mokkenstorm/crate/catalog/adoptRelease.sdl b/lexicons/dev/mokkenstorm/crate/catalog/adoptRelease.sdl index 6a9af11..3ee46fc 100644 --- a/lexicons/dev/mokkenstorm/crate/catalog/adoptRelease.sdl +++ b/lexicons/dev/mokkenstorm/crate/catalog/adoptRelease.sdl @@ -1,11 +1,10 @@ /// Adopt a browsed catalog release into the caller's crate as a genesis shelf event. procedure adoptRelease() @main accepts { - /// Expected CID, re-verified against the live record before writing. - cid: Cid! /// Defaults to owned when absent. status: String @known("owned", "wanted") - uri: AtUri! + /// The release to adopt; its CID is re-verified against the live record before writing. + subject: com.atproto.repo.strongRef! } : { /// TID of the new entry's genesis event. diff --git a/lexicons/dev/mokkenstorm/crate/catalog/applyEditProposal.sdl b/lexicons/dev/mokkenstorm/crate/catalog/applyEditProposal.sdl index 204cd6d..6c5e394 100644 --- a/lexicons/dev/mokkenstorm/crate/catalog/applyEditProposal.sdl +++ b/lexicons/dev/mokkenstorm/crate/catalog/applyEditProposal.sdl @@ -1,11 +1,7 @@ /// Applies a proposal: mints a superseding release and re-points shelf entries; the proposal record itself is untouched. procedure applyEditProposal() @main accepts { - /// Expected CID, re-verified against the live proposal record before applying. - cid: Cid! - uri: AtUri! - } - : { - releaseCid: Cid! - releaseUri: AtUri! + /// The proposal to apply; its CID is re-verified against the live record before applying. + subject: com.atproto.repo.strongRef! } + : com.atproto.repo.strongRef diff --git a/lexicons/dev/mokkenstorm/crate/defs.sdl b/lexicons/dev/mokkenstorm/crate/defs.sdl index 2343994..72f87c3 100644 --- a/lexicons/dev/mokkenstorm/crate/defs.sdl +++ b/lexicons/dev/mokkenstorm/crate/defs.sdl @@ -24,6 +24,17 @@ type price { currency: String! @len(max: 16) @graphemes(max: 8) } +/// Shared display fields resolved from a catalog release, all optional so any render site can carry the subset it has. +type releaseDisplay { + artistDisplay: String + country: String + genres: [String] + released: String + styles: [String] + thumbUrl: Uri + title: String +} + /// Denormalized display copy so a record renders without resolving its target. type snapshot { artistDisplay: String! @len(max: 2560) @graphemes(max: 256) diff --git a/lexicons/dev/mokkenstorm/crate/shelf/addEntry.sdl b/lexicons/dev/mokkenstorm/crate/shelf/addEntry.sdl index 7dccf2e..a3f9f57 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/addEntry.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/addEntry.sdl @@ -9,7 +9,7 @@ procedure addEntry() @main discogsId: String folder: String @len(max: 1280) @graphemes(max: 128) format: String - mediaGrade: String + mediaGrade: String @known("M", "NM", "VG+", "VG", "G+", "G", "F", "P") notes: String price: dev.mokkenstorm.crate.defs#price rating: Int @range(1, 5) @@ -20,8 +20,4 @@ procedure addEntry() @main title: String! year: Int } - : { - cid: Cid! - entryId: String! - uri: AtUri! - } + : dev.mokkenstorm.crate.shelf.defs#writeAck diff --git a/lexicons/dev/mokkenstorm/crate/shelf/amendEntry.sdl b/lexicons/dev/mokkenstorm/crate/shelf/amendEntry.sdl index 8bd2ae3..1b98c6e 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/amendEntry.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/amendEntry.sdl @@ -1,16 +1,10 @@ /// Correct an entry's release fields, minting a new catalog release and re-pointing the entry via an updated event. procedure amendEntry() @main accepts { - country: String entryId: String! - genres: [String] + /// Corrected release columns, shared with catalog edit proposals. + fields: dev.mokkenstorm.crate.catalog.edit#releaseFields /// Re-fetch cover art from the release's own Discogs id. refreshCover: Bool - released: String - styles: [String] - title: String - } - : { - entryId: String! - releaseUri: AtUri! } + : dev.mokkenstorm.crate.shelf.defs#amendAck diff --git a/lexicons/dev/mokkenstorm/crate/shelf/appendEvent.sdl b/lexicons/dev/mokkenstorm/crate/shelf/appendEvent.sdl index 9a501fe..cf1d8a1 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/appendEvent.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/appendEvent.sdl @@ -1,17 +1,13 @@ /// Append an immutable action event to an entry. procedure appendEvent() @main accepts { - action: String! @known("acquired", "wanted", "regraded", "rated", "moved", "annotated", "sold", "dropped") + action: String! @known("wanted", "acquired", "regraded", "rated", "moved", "annotated", "updated", "sold", "dropped") /// The genesis TID of the entry to append to. entryId: String! folder: String - mediaGrade: String + mediaGrade: String @known("M", "NM", "VG+", "VG", "G+", "G", "F", "P") notes: String rating: Int - sleeveGrade: String - } - : { - cid: Cid! - entryId: String! - uri: AtUri! + sleeveGrade: String @known("M", "NM", "VG+", "VG", "G+", "G", "F", "P") } + : dev.mokkenstorm.crate.shelf.defs#writeAck diff --git a/lexicons/dev/mokkenstorm/crate/shelf/defs.sdl b/lexicons/dev/mokkenstorm/crate/shelf/defs.sdl new file mode 100644 index 0000000..379bb8e --- /dev/null +++ b/lexicons/dev/mokkenstorm/crate/shelf/defs.sdl @@ -0,0 +1,12 @@ +/// Acknowledges an amendment that minted a new catalog release and re-pointed the entry. +type amendAck { + entryId: String! + releaseUri: AtUri! +} + +/// Acknowledges an appended shelf event, identifying both the written record and its entry. +type writeAck { + cid: Cid! + entryId: String! + uri: AtUri! +} diff --git a/lexicons/dev/mokkenstorm/crate/shelf/getEntry.sdl b/lexicons/dev/mokkenstorm/crate/shelf/getEntry.sdl index 31a4816..9a22f13 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/getEntry.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/getEntry.sdl @@ -5,13 +5,5 @@ query getEntry(entry: String! @description("The entry's genesis TID.")) @main /// Every shelf event for the entry, oldest first. events: [dev.mokkenstorm.crate.shelf.entry]! /// Display fields from the resolved catalog release, omitted when unresolvable. - release: releaseInfo + release: dev.mokkenstorm.crate.defs#releaseDisplay } - -type releaseInfo { - artistDisplay: String - country: String - genres: [String] - released: String - styles: [String] -} diff --git a/lexicons/dev/mokkenstorm/crate/shelf/getPublicEntry.sdl b/lexicons/dev/mokkenstorm/crate/shelf/getPublicEntry.sdl index 062e4ce..68692b8 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/getPublicEntry.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/getPublicEntry.sdl @@ -8,5 +8,5 @@ query getPublicEntry(actor: AtIdentifier! @description("Handle or DID of the cra handle: Handle! entry: dev.mokkenstorm.crate.shelf.listEntries#entry! /// Display fields from the resolved catalog release, omitted when unresolvable. - release: dev.mokkenstorm.crate.shelf.getEntry#releaseInfo + release: dev.mokkenstorm.crate.defs#releaseDisplay } diff --git a/lexicons/dev/mokkenstorm/crate/shelf/uploadCover.sdl b/lexicons/dev/mokkenstorm/crate/shelf/uploadCover.sdl index 938b4ae..d85e300 100644 --- a/lexicons/dev/mokkenstorm/crate/shelf/uploadCover.sdl +++ b/lexicons/dev/mokkenstorm/crate/shelf/uploadCover.sdl @@ -1,7 +1,4 @@ /// Upload a cover image for an entry as a cover-only amendment. procedure uploadCover(entry: String! @description("The entry's genesis TID.")) @main accepts Blob @encoding("multipart/form-data") @description("A single image file, at most 2MB.") - : { - entryId: String! - releaseUri: AtUri! - } + : dev.mokkenstorm.crate.shelf.defs#amendAck diff --git a/server/src/at_record_server/handlers/amend.gleam b/server/src/at_record_server/handlers/amend.gleam index 4326b4c..69ce3bf 100644 --- a/server/src/at_record_server/handlers/amend.gleam +++ b/server/src/at_record_server/handlers/amend.gleam @@ -62,49 +62,30 @@ pub fn form_from_release_fields( ) } -fn form_decoder() -> decode.Decoder(AmendForm) { - let strings = decode.optional(decode.list(decode.string)) - use title <- decode.optional_field( - "title", - None, - decode.optional(decode.string), - ) - use released <- decode.optional_field( - "released", - None, - decode.optional(decode.string), - ) - use country <- decode.optional_field( - "country", - None, - decode.optional(decode.string), - ) - use genres <- decode.optional_field("genres", None, strings) - use styles <- decode.optional_field("styles", None, strings) - use refresh_cover <- decode.optional_field("refreshCover", False, decode.bool) - decode.success(AmendForm( - title:, - released:, - country:, - genres:, - styles:, - refresh_cover:, - )) -} - pub fn amend_entry(req: Request, ctx: Context) -> Response { use id, session <- require_session(req, ctx) use body <- wisp.require_json(req) case decode.run(body, amend_request_decoder()) { - Error(_) -> error_json(400, "expected { entryId, ...amended fields }") + Error(_) -> error_json(400, "expected { entryId, fields, refreshCover }") Ok(#(entry_id, form)) -> do_amend(ctx, id, session, entry_id, form, None) } } +// The release columns arrive nested under `fields`, shared with catalog.edit's +// releaseFields; entryId and refreshCover stay local to the amend procedure. fn amend_request_decoder() -> decode.Decoder(#(String, AmendForm)) { use entry_id <- decode.field("entryId", decode.string) - use form <- decode.then(form_decoder()) - decode.success(#(entry_id, form)) + use fields <- decode.optional_field( + "fields", + None, + decode.optional(catalog_edit.release_fields_decoder()), + ) + use refresh_cover <- decode.optional_field("refreshCover", False, decode.bool) + let base = case fields { + Some(f) -> form_from_release_fields(f) + None -> empty_form() + } + decode.success(#(entry_id, AmendForm(..base, refresh_cover:))) } /// A photographed or uploaded cover: a cover-only amendment. The bytes become diff --git a/server/src/at_record_server/handlers/browse.gleam b/server/src/at_record_server/handlers/browse.gleam index 7a1983a..fff5b09 100644 --- a/server/src/at_record_server/handlers/browse.gleam +++ b/server/src/at_record_server/handlers/browse.gleam @@ -165,17 +165,16 @@ type AddForm { } fn add_decoder() -> decode.Decoder(AddForm) { - use uri <- decode.field("uri", decode.string) - use cid <- decode.field("cid", decode.string) + use subject <- decode.field("subject", strong_ref.repo_strong_ref_decoder()) use status <- decode.optional_field("status", "owned", decode.string) - decode.success(AddForm(uri:, cid:, status:)) + decode.success(AddForm(uri: subject.uri, cid: subject.cid, status:)) } pub fn add(req: Request, ctx: Context) -> Response { use id, session <- require_session(req, ctx) use body <- wisp.require_json(req) case decode.run(body, add_decoder()) { - Error(_) -> error_json(400, "expected { uri, cid, status }") + Error(_) -> error_json(400, "expected { subject: { uri, cid }, status }") Ok(form) -> do_add(ctx, id, session, form) } } diff --git a/server/src/at_record_server/handlers/edit_inbox.gleam b/server/src/at_record_server/handlers/edit_inbox.gleam index 6459225..b998e91 100644 --- a/server/src/at_record_server/handlers/edit_inbox.gleam +++ b/server/src/at_record_server/handlers/edit_inbox.gleam @@ -10,6 +10,7 @@ import at_record/gen/catalog/edit as catalog_edit import at_record/gen/catalog/list_edit_proposals import at_record/gen/defs +import at_record/gen/repo/strong_ref import at_record/gen/shelf/entry.{type ShelfEntry} import at_record/storage.{type StoredItem} import at_record_server/context.{ @@ -212,16 +213,15 @@ type ApplyForm { } fn apply_form_decoder() -> decode.Decoder(ApplyForm) { - use uri <- decode.field("uri", decode.string) - use cid <- decode.field("cid", decode.string) - decode.success(ApplyForm(uri:, cid:)) + use subject <- decode.field("subject", strong_ref.repo_strong_ref_decoder()) + decode.success(ApplyForm(uri: subject.uri, cid: subject.cid)) } pub fn apply_proposal(req: Request, ctx: Context) -> Response { use id, session <- require_session(req, ctx) use body <- wisp.require_json(req) case decode.run(body, apply_form_decoder()) { - Error(_) -> error_json(400, "expected { uri, cid }") + Error(_) -> error_json(400, "expected { subject: { uri, cid } }") Ok(form) -> do_apply(ctx, id, session, form) } } @@ -236,8 +236,8 @@ fn do_apply( case apply(ctx, client, session, form.uri, form.cid) { Ok(Applied(release:)) -> json.object([ - #("releaseUri", json.string(release.uri)), - #("releaseCid", json.string(release.cid)), + #("cid", json.string(release.cid)), + #("uri", json.string(release.uri)), ]) |> json.to_string |> wisp.json_response(201) diff --git a/web/src/at_record_web/effects.gleam b/web/src/at_record_web/effects.gleam index 03e171e..fb5d73f 100644 --- a/web/src/at_record_web/effects.gleam +++ b/web/src/at_record_web/effects.gleam @@ -7,7 +7,6 @@ import at_record/gen/discogs/resolve_barcode as discogs_resolve_barcode import at_record/gen/discogs/search_artists as discogs_search_artists import at_record/gen/discogs/search_releases as discogs_search_releases import at_record/gen/shelf/entry.{shelf_entry_decoder} -import at_record/gen/shelf/get_entry as shelf_get_entry import at_record_web/appview import at_record_web/browser import at_record_web/model.{ @@ -530,18 +529,22 @@ pub fn amend_entry(entry_id: String, draft: AmendDraft) -> Effect(Msg) { items -> [#(key, json.array(items, json.string))] } } - let body = + let fields = json.object( list.flatten([ - [#("entryId", json.string(entry_id))], str("title", draft.title), str("released", draft.released), str("country", draft.country), listed("genres", draft.genres), listed("styles", draft.styles), - [#("refreshCover", json.bool(draft.refresh_cover))], ]), ) + let body = + json.object([ + #("entryId", json.string(entry_id)), + #("fields", fields), + #("refreshCover", json.bool(draft.refresh_cover)), + ]) rsvp.post( xrpc("shelf.amendEntry"), body, @@ -581,8 +584,7 @@ pub fn adopt_scanned( ) -> Effect(Msg) { let body = json.object([ - #("uri", json.string(uri)), - #("cid", json.string(cid)), + #("subject", strong_ref(uri, cid)), #("status", json.string("owned")), ]) rsvp.post( @@ -683,8 +685,7 @@ pub fn load_pressing(release_uri: String) -> Effect(Msg) { pub fn browse_add(uri: String, cid: String, status: String) -> Effect(Msg) { let body = json.object([ - #("uri", json.string(uri)), - #("cid", json.string(cid)), + #("subject", strong_ref(uri, cid)), #("status", json.string(status)), ]) rsvp.post( @@ -694,6 +695,12 @@ pub fn browse_add(uri: String, cid: String, status: String) -> Effect(Msg) { ) } +/// A `com.atproto.repo.strongRef` body fragment: a uri paired with its +/// content-hash cid. +fn strong_ref(uri: String, cid: String) -> Json { + json.object([#("uri", json.string(uri)), #("cid", json.string(cid))]) +} + /// Edit proposals filed against the caller's own catalog releases; loaded /// when the inbox page opens, never at login. pub fn load_edit_inbox() -> Effect(Msg) { @@ -758,10 +765,9 @@ fn proposal_fields_or_blank( /// Mint a superseding release from the proposal's fields and re-point every /// shelf entry that referenced the old one. pub fn apply_edit_proposal(uri: String, cid: String) -> Effect(Msg) { - let body = - json.object([#("uri", json.string(uri)), #("cid", json.string(cid))]) + let body = json.object([#("subject", strong_ref(uri, cid))]) let decoder = { - use release_uri <- decode.field("releaseUri", decode.string) + use release_uri <- decode.field("uri", decode.string) decode.success(AppliedProposal(release_uri:)) } rsvp.post( @@ -820,7 +826,7 @@ pub fn load_timeline(entry_id: String) -> Effect(Msg) { } fn release_info_decoder() -> decode.Decoder(ReleaseInfo) { - decode.map(shelf_get_entry.release_info_decoder(), fn(info) { + decode.map(defs.release_display_decoder(), fn(info) { ReleaseInfo( artist_display: info.artist_display, genres: option.unwrap(info.genres, []),