grain.social is a photo sharing platform built on atproto.

feat: add cascade option to deleteGallery and deletePhoto procedures to remove associated items

Changed files
+126 -72
__generated__
types
social
grain
lexicons
social
grain
src
+12
__generated__/lexicons.ts
··· 2816 2816 format: 'at-uri', 2817 2817 description: 'Unique identifier of the gallery to delete', 2818 2818 }, 2819 + cascade: { 2820 + type: 'boolean', 2821 + default: true, 2822 + description: 2823 + 'If true, will also delete any associated items in the gallery.', 2824 + }, 2819 2825 }, 2820 2826 }, 2821 2827 }, ··· 4404 4410 type: 'string', 4405 4411 format: 'at-uri', 4406 4412 description: 'AT URI of the photo to delete.', 4413 + }, 4414 + cascade: { 4415 + type: 'boolean', 4416 + default: true, 4417 + description: 4418 + 'If true, will also delete any associated EXIF data and gallery items.', 4407 4419 }, 4408 4420 }, 4409 4421 },
+2
__generated__/types/social/grain/gallery/deleteGallery.ts
··· 15 15 export interface InputSchema { 16 16 /** Unique identifier of the gallery to delete */ 17 17 uri: string; 18 + /** If true, will also delete any associated items in the gallery. */ 19 + cascade: boolean; 18 20 } 19 21 20 22 export interface OutputSchema {
+2
__generated__/types/social/grain/photo/deletePhoto.ts
··· 15 15 export interface InputSchema { 16 16 /** AT URI of the photo to delete. */ 17 17 uri: string; 18 + /** If true, will also delete any associated EXIF data and gallery items. */ 19 + cascade: boolean; 18 20 } 19 21 20 22 export interface OutputSchema {
+5
lexicons/social/grain/gallery/deleteGallery.json
··· 15 15 "type": "string", 16 16 "format": "at-uri", 17 17 "description": "Unique identifier of the gallery to delete" 18 + }, 19 + "cascade": { 20 + "type": "boolean", 21 + "default": true, 22 + "description": "If true, will also delete any associated items in the gallery." 18 23 } 19 24 } 20 25 }
+5
lexicons/social/grain/photo/deletePhoto.json
··· 15 15 "type": "string", 16 16 "format": "at-uri", 17 17 "description": "AT URI of the photo to delete." 18 + }, 19 + "cascade": { 20 + "type": "boolean", 21 + "default": true, 22 + "description": "If true, will also delete any associated EXIF data and gallery items." 18 23 } 19 24 } 20 25 }
+15 -13
src/api/mod.ts
··· 138 138 getFollowingWithProfiles, 139 139 } from "../lib/graph.ts"; 140 140 import { getNotificationsDetailed } from "../lib/notifications.ts"; 141 - import { applyAlts, createExif, createPhoto } from "../lib/photo.ts"; 141 + import { 142 + applyAlts, 143 + createExif, 144 + createPhoto, 145 + deletePhoto, 146 + } from "../lib/photo.ts"; 142 147 import { getTimeline } from "../lib/timeline.ts"; 143 148 import { createComment, getGalleryComments } from "../modules/comments.tsx"; 144 149 ··· 181 186 ["POST"], 182 187 async (req, _params, ctx) => { 183 188 ctx.requireAuth(); 184 - const { uri } = await parseDeleteGalleryInputs( 189 + const { uri, cascade = true } = await parseDeleteGalleryInputs( 185 190 req, 186 191 ); 187 - const success = await deleteGallery(uri, ctx); 192 + const success = await deleteGallery(uri, cascade, ctx); 188 193 return ctx.json({ success } satisfies DeleteGalleryOutputSchema); 189 194 }, 190 195 ), ··· 298 303 ["POST"], 299 304 async (req, _params, ctx) => { 300 305 ctx.requireAuth(); 301 - const { uri } = await parseDeletePhotoInputs(req); 302 - try { 303 - await ctx.deleteRecord(uri); 304 - return ctx.json({ success: true } satisfies DeletePhotoOutputSchema); 305 - } catch (error) { 306 - console.error("Error deleting photo:", error); 307 - return ctx.json({ success: false } satisfies DeletePhotoOutputSchema); 308 - } 306 + const { uri, cascade = true } = await parseDeletePhotoInputs(req); 307 + const success = await deletePhoto(uri, cascade, ctx); 308 + return ctx.json({ success } satisfies DeletePhotoOutputSchema); 309 309 }, 310 310 ), 311 311 route( ··· 772 772 if (!uri) { 773 773 throw new XRPCError("InvalidRequest", "Missing uri input"); 774 774 } 775 - return { uri }; 775 + const cascade = typeof body.cascade === "boolean" ? body.cascade : undefined; 776 + return { uri, cascade }; 776 777 } 777 778 778 779 async function parseCreateGalleryItemInputs( ··· 819 820 if (!uri) { 820 821 throw new XRPCError("InvalidRequest", "Missing uri input"); 821 822 } 822 - return { uri }; 823 + const cascade = typeof body.cascade === "boolean" ? body.cascade : undefined; 824 + return { uri, cascade }; 823 825 } 824 826 825 827 async function parseCreateFollowInputs(
+6 -1
src/lib/gallery.ts
··· 145 145 }); 146 146 } 147 147 148 - export async function deleteGallery(uri: string, ctx: BffContext) { 148 + export async function deleteGallery( 149 + uri: string, 150 + cascade: boolean, 151 + ctx: BffContext, 152 + ) { 149 153 try { 150 154 await ctx.deleteRecord(uri); 155 + if (!cascade) return true; 151 156 const { items: galleryItems } = ctx.indexService.getRecords< 152 157 WithBffMeta<GalleryItem> 153 158 >("social.grain.gallery.item", {
+70
src/lib/photo.ts
··· 1 + import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts"; 1 2 import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 3 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 3 4 import { Record as GalleryItem } from "$lexicon/types/social/grain/gallery/item.ts"; ··· 351 352 352 353 return true; 353 354 } 355 + 356 + export async function deletePhoto( 357 + uri: string, 358 + cascade: boolean, 359 + ctx: BffContext, 360 + ): Promise<boolean> { 361 + const deleteUris: string[] = []; 362 + try { 363 + await ctx.deleteRecord( 364 + uri, 365 + ); 366 + if (!cascade) return true; 367 + const { items: galleryItems } = ctx.indexService.getRecords< 368 + WithBffMeta<GalleryItem> 369 + >( 370 + "social.grain.gallery.item", 371 + { 372 + where: [ 373 + { 374 + field: "item", 375 + equals: uri, 376 + }, 377 + ], 378 + }, 379 + ); 380 + for (const item of galleryItems) { 381 + deleteUris.push(item.uri); 382 + } 383 + const { items: favorites } = ctx.indexService.getRecords< 384 + WithBffMeta<Favorite> 385 + >( 386 + "social.grain.favorite", 387 + { 388 + where: [ 389 + { 390 + field: "subject", 391 + equals: uri, 392 + }, 393 + ], 394 + }, 395 + ); 396 + for (const favorite of favorites) { 397 + deleteUris.push(favorite.uri); 398 + } 399 + const { items: exifItems } = ctx.indexService.getRecords< 400 + WithBffMeta<PhotoExif> 401 + >( 402 + "social.grain.photo.exif", 403 + { 404 + where: [ 405 + { 406 + field: "photo", 407 + equals: uri, 408 + }, 409 + ], 410 + }, 411 + ); 412 + for (const item of exifItems) { 413 + deleteUris.push(item.uri); 414 + } 415 + for (const uri of deleteUris) { 416 + await ctx.deleteRecord(uri); 417 + } 418 + } catch (error) { 419 + console.error("Failed to delete photo:", error); 420 + return false; 421 + } 422 + return true; 423 + }
+9 -58
src/routes/actions.tsx
··· 5 5 import { Record as GalleryItem } from "$lexicon/types/social/grain/gallery/item.ts"; 6 6 import { Record as Photo } from "$lexicon/types/social/grain/photo.ts"; 7 7 import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 8 - import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 9 8 import { AtUri } from "@atproto/syntax"; 10 9 import { BffContext, RouteHandler, WithBffMeta } from "@bigmoves/bff"; 11 10 import { ··· 28 27 updateGallery, 29 28 } from "../lib/gallery.ts"; 30 29 import { getFollowers } from "../lib/graph.ts"; 31 - import { createExif, createPhoto, getPhoto } from "../lib/photo.ts"; 30 + import { 31 + createExif, 32 + createPhoto, 33 + deletePhoto, 34 + getPhoto, 35 + } from "../lib/photo.ts"; 32 36 import type { State } from "../state.ts"; 33 37 import { galleryLink, profileLink, uploadPageLink } from "../utils.ts"; 34 38 ··· 131 135 const { handle } = ctx.requireAuth(); 132 136 const formData = await req.formData(); 133 137 const uri = formData.get("uri") as string; 134 - await deleteGallery(uri, ctx); 138 + await deleteGallery(uri, true, ctx); 135 139 return ctx.redirect(profileLink(handle)); 136 140 }; 137 141 ··· 331 335 const selectedGalleryRkey = selectedGallery 332 336 ? new AtUri(selectedGallery).rkey 333 337 : undefined; 334 - const deleteUris: string[] = []; 335 - await ctx.deleteRecord( 336 - `at://${did}/social.grain.photo/${params.rkey}`, 337 - ); 338 - const { items: galleryItems } = ctx.indexService.getRecords< 339 - WithBffMeta<GalleryItem> 340 - >( 341 - "social.grain.gallery.item", 342 - { 343 - where: [ 344 - { 345 - field: "item", 346 - equals: `at://${did}/social.grain.photo/${params.rkey}`, 347 - }, 348 - ], 349 - }, 350 - ); 351 - for (const item of galleryItems) { 352 - deleteUris.push(item.uri); 353 - } 354 - const { items: favorites } = ctx.indexService.getRecords< 355 - WithBffMeta<Favorite> 356 - >( 357 - "social.grain.favorite", 358 - { 359 - where: [ 360 - { 361 - field: "subject", 362 - equals: `at://${did}/social.grain.photo/${params.rkey}`, 363 - }, 364 - ], 365 - }, 366 - ); 367 - for (const favorite of favorites) { 368 - deleteUris.push(favorite.uri); 369 - } 370 - const { items: exifItems } = ctx.indexService.getRecords< 371 - WithBffMeta<PhotoExif> 372 - >( 373 - "social.grain.photo.exif", 374 - { 375 - where: [ 376 - { 377 - field: "photo", 378 - equals: `at://${did}/social.grain.photo/${params.rkey}`, 379 - }, 380 - ], 381 - }, 382 - ); 383 - for (const item of exifItems) { 384 - deleteUris.push(item.uri); 385 - } 386 - for (const uri of deleteUris) { 387 - await ctx.deleteRecord(uri); 388 - } 338 + const photoUri = `at://${did}/social.grain.photo/${params.rkey}`; 339 + await deletePhoto(photoUri, true, ctx); 389 340 return ctx.redirect(uploadPageLink(selectedGalleryRkey)); 390 341 }; 391 342