From 570b64ff3fbcc32a2d869226215f972c45daf917 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Wed, 29 May 2024 09:51:03 -0400 Subject: [PATCH 01/18] rename `userPools` to `userPool` --- .../build-a-backend/data/customize-authz/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx b/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx index a840ddd7a5d..63c53d6d369 100644 --- a/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/customize-authz/index.mdx @@ -55,12 +55,12 @@ In the example above, everyone (`public`) can read every Post but authenticated Use the guide below to select the correct authorization strategy for your use case: | **Recommended use case** | **Strategy** | **`authMode`** | -|---|---|---| +|--------------------------|--------------|----------------| | [Public data access where users or devices are anonymous. Anyone with the AppSync API key is granted access.](/[platform]/build-a-backend/data/customize-authz/public-data-access) | `publicApiKey` | `apiKey` | | [Recommended for production environment's public data access. Public data access where unauthenticated users or devices are granted permissions using Amazon Cognito identity pool's role for unauthenticated identities.]( /[platform]/build-a-backend/data/customize-authz/public-data-access/#add-public-authorization-rule-using-iam-authentication) | `guest` | `identityPool` | -| [Per user data access. Access is restricted to the "owner" of a record. Leverages `amplify/auth/resource.ts` Cognito user pool by default.](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access) | `owner`/`ownerDefinedIn`/`ownersDefinedIn` | `userPools` / `oidc` | -| [Any signed-in data access. Unlike owner-based access, **any** signed-in user has access.](/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access) | `authenticated` | `userPools` / `oidc` / `identityPool` | -| [Per user group data access. A specific or dynamically configured group of users has access.](/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access) | `group`/`groupDefinedIn`/`groups`/`groupsDefinedIn` | `userPools` / `oidc` | +| [Per user data access. Access is restricted to the "owner" of a record. Leverages `amplify/auth/resource.ts` Cognito user pool by default.](/[platform]/build-a-backend/data/customize-authz/per-user-per-owner-data-access) | `owner`/`ownerDefinedIn`/`ownersDefinedIn` | `userPool` / `oidc` | +| [Any signed-in data access. Unlike owner-based access, **any** signed-in user has access.](/[platform]/build-a-backend/data/customize-authz/signed-in-user-data-access) | `authenticated` | `userPool` / `oidc` / `identityPool` | +| [Per user group data access. A specific or dynamically configured group of users has access.](/[platform]/build-a-backend/data/customize-authz/user-group-based-data-access) | `group`/`groupDefinedIn`/`groups`/`groupsDefinedIn` | `userPool` / `oidc` | | [Define your own custom authorization rule within a serverless function.](/[platform]/build-a-backend/data/customize-authz/custom-data-access-patterns) | `custom` | `lambda` | ## Understand how authorization rules are applied From 4f76a24affbbb751d2134c463cdbc91e82ecab30 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 31 May 2024 13:34:54 -0400 Subject: [PATCH 02/18] add inline filters for Swift --- .../data/working-with-files/index.mdx | 115 +++++++++++++++++- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 0929420804f..9a6ab607e8f 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -124,6 +124,67 @@ The API record's `id` is prepended to the Storage file name to ensure uniqueness + +```swift title="ContentView" +import Amplify + +// Create the API record: +var song = Song(name: "My first song") + +do { + let result = try await Amplify.API.mutate(request: .create(song)) + + switch result { + case .success(let newSong): + song = newSong + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } +} catch let error as APIError { + print("Failed to create song: ", error) +} catch { + print("Unexpected error: \(error)") +} + +// Upload the Storage file: +let uploadTask = Amplify.Storage.uploadData( + path: .fromString("images/\(song.id)-cover.png"), + data: imageData +) + +let key = try await uploadTask.value + +// Add the file association to the record: +var updatedSong = Song(id: song.id, name: song.name, coverArtPath: key) + +do { + let updateResult = try await Amplify.API.mutate(request: .update(updatedSong)) + + switch updateResult { + case .success(let song): + updatedSong = song + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } +} catch let error as APIError { + print("Failed to create song: ", error) +} catch { + print("Unexpected error: \(error)") +} + +// If the record has no associated file, we can return early. +if (updatedSong.coverArtPath == nil) { + print("No associated file") + return +}; + +// Retrieve the file's signed URL: +let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) +``` + + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; @@ -168,14 +229,58 @@ if (!updatedSong.coverArtPath) return; // Retrieve the file's signed URL: const signedURL = await getUrl({ path: updatedSong.coverArtPath }); - - ``` + ## Add or update a file for an associated record To associate a file with a record, update the record with the key returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's key, then retrieves the signed URL to download the image. If an image is already associated with the record, this will update the record with the new image. + +```swift title="ContentView" +import Amplify + +// Upload the Storage file: +let uploadTask = Amplify.Storage.uploadData( + path: .fromString("images/\(song.id)-cover.png"), + data: imageData +) + +let path = try await uploadTask.value + +// Add the file association to the record: +var updatedSong = Song(id: song.id, name: song.name, coverArtPath: path) + +do { + let updateResult = try await Amplify.API.mutate(request: .update(updatedSong)) + + switch updateResult { + case .success(let song): + updatedSong = song + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } +} catch let error as APIError { + print("Failed to create song: ", error) +} catch { + print("Unexpected error: \(error)") +} + +// If the record has no associated file, we can return early. +if (updatedSong.coverArtPath == nil) { + print("No associated file") + return +}; + +// Retrieve the file's signed URL: +let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) +``` + + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { uploadData, getUrl } from "aws-amplify/storage"; @@ -213,6 +318,8 @@ const signedURL = await getUrl({ path: updatedSong.coverArtPath }); ``` + + ## Query a record and retrieve the associated file To retrieve the file associated with a record, first query the record, then use Storage to get the signed URL. The signed URL can then be used to download the file, display an image, etc: @@ -736,7 +843,7 @@ function App({ signOut, user }: WithAuthenticatorProps) { const [currentImageUrl, setCurrentImageUrl] = useState< string | null | undefined >(""); - + async function createSongWithImage(e: React.ChangeEvent) { if (!e.target.files) return; const file = e.target.files[0]; @@ -926,7 +1033,7 @@ function App({ signOut, user }: WithAuthenticatorProps) { // Delete the record from the API: await client.models.Song.delete({ id: song.id }); - + clearLocalState(); } catch (error) { From 964927d89f79eb3aa2d5866d0bd9bea64e216682 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 31 May 2024 14:28:43 -0400 Subject: [PATCH 03/18] update Query a record and retrieve the associated file example --- .../data/working-with-files/index.mdx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 9a6ab607e8f..fd90438a4a0 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -315,7 +315,6 @@ if (!updatedSong?.coverArtPath) return; // Retrieve the file's signed URL: const signedURL = await getUrl({ path: updatedSong.coverArtPath }); - ``` @@ -323,7 +322,39 @@ const signedURL = await getUrl({ path: updatedSong.coverArtPath }); ## Query a record and retrieve the associated file To retrieve the file associated with a record, first query the record, then use Storage to get the signed URL. The signed URL can then be used to download the file, display an image, etc: + +```swift title="ContentView" +import Amplify +do { + let result = try await Amplify.API.query(request: .get(Song.self, byId: song.id)) + + switch result { + case .success(let song): + + // If the record has no associated file, we can return early. + if (song?.coverArtPath == nil) { + print("No associated file") + return + } else { + + // Retrieve the file's signed URL: + let signedUrl = try await Amplify.Storage.getURL(key: song!.coverArtPath!) + } + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } +} catch let error as APIError { + print("Failed to retrieve song: ", error) +} catch { + print("Unexpected error: \(error)") +} +``` + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { getUrl } from "aws-amplify/storage"; @@ -337,6 +368,7 @@ const client = generateClient({ const response = await client.models.Song.get({ id: currentSong.id, }); + const song = response.data; // If the record has no associated file, we can return early. @@ -346,6 +378,7 @@ if (!song?.coverArtPath) return; const signedURL = await getUrl({ path: song.coverArtPath }); ``` + ## Delete and remove files associated with API records From 98195fd6b589a3bfec7386f8054c9cbca2b36fec Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Mon, 10 Jun 2024 12:04:40 -0400 Subject: [PATCH 04/18] update with examples in swift, add infline filter for complete examples --- .../data/working-with-files/index.mdx | 521 +++++++++++++++++- 1 file changed, 494 insertions(+), 27 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index fd90438a4a0..df8b76ddeaf 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -128,7 +128,7 @@ The API record's `id` is prepended to the Storage file name to ensure uniqueness ```swift title="ContentView" import Amplify -// Create the API record: + // Create the API record: var song = Song(name: "My first song") do { @@ -152,13 +152,13 @@ let uploadTask = Amplify.Storage.uploadData( data: imageData ) -let key = try await uploadTask.value +do { + let key = try await uploadTask.value -// Add the file association to the record: -var updatedSong = Song(id: song.id, name: song.name, coverArtPath: key) + // Add the file association to the record: + var updatedSong = Song(id: song.id, name: song.name, coverArtPath: key) -do { - let updateResult = try await Amplify.API.mutate(request: .update(updatedSong)) + let updateResult = try await Amplify.API.mutate(request: .update(Song(id: updatedSong.id, name: song.name, coverArtPath: nil))) switch updateResult { case .success(let song): @@ -166,20 +166,18 @@ do { case .failure(let error): print("Got failed result with \(error.errorDescription)") } -} catch let error as APIError { - print("Failed to create song: ", error) -} catch { - print("Unexpected error: \(error)") -} -// If the record has no associated file, we can return early. -if (updatedSong.coverArtPath == nil) { - print("No associated file") - return -}; + // If the record has no associated file, we can return early. + if (updatedSong.coverArtPath == nil) { + print("No associated file") + return + }; -// Retrieve the file's signed URL: -let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) + // Retrieve the file's signed URL: + let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) + + print(signedUrl) +} ``` @@ -392,6 +390,32 @@ There are three common deletion workflows when working with Storage files and th The following example removes the file association from the record, but does not delete the file from S3, nor the record from the database. + +```swift title="ContentView" +import Amplify + +do { + let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) + + // If the record has no associated file, we can return early. + if (currentSong.coverArtPath == nil) { + return + } + + // Remove associated file from record + let updatedSong = try await Amplify.API.mutate(request: .update(Song(id: currentSong.id, name: currentSong.name, coverArtPath: nil))) + +} catch let error as APIError { + print("Failed to remove photo association: ", error) +} catch { + print("Unexpected error: \(error)") +} +``` + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; @@ -417,10 +441,42 @@ const updatedSong = await client.models.Song.update({ }); ``` + + ### Remove the record association and delete the file The following example removes the file from the record, then deletes the file from S3: + +```swift title="ContentView" +import Amplify + +do { + let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) + + // If the record has no associated file, we can return early. + if (currentSong.coverArtPath == nil) { + return + } + + // Remove associated file from record + let updatedSong = try await Amplify.API.mutate(request: .update(Song(id: currentSong.id, name: currentSong.id, coverArtPath: nil))) + + // Delete the file from S3 + try await Amplify.Storage.remove(key: currentSong.coverArtPath!) + +} catch let error as APIError { + print("Failed to remove photo association: ", error) +} catch { + print("Unexpected error: \(error)") +} +``` + + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { remove } from "aws-amplify/storage"; @@ -448,9 +504,39 @@ const updatedSong = await client.models.Song.update({ await remove({ path: song.coverArtPath }); ``` + ### Delete both file and record + +```swift title="ContentView" +import Amplify + +do { + let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) + + // If the record has no associated file, we can return early. + if (currentSong.coverArtPath == nil) { + return + } + + // Delete the record from the API + try await Amplify.API.mutate(request: .delete(Song.self, byId: currentSong.id)) + + // Delete the file from S3 + try await Amplify.Storage.remove(key: currentSong.coverArtPath!) + +} catch let error as APIError { + print("Failed to remove photo association: ", error) +} catch { + print("Unexpected error: \(error)") +} +``` + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { remove } from "aws-amplify/storage"; @@ -475,6 +561,7 @@ await remove({ path: song.coverArtPath }); await client.models.Song.delete({ id: song.id }); ``` + ## Working with multiple files @@ -502,8 +589,91 @@ CRUD operations when working with multiple files is the same as when working wit First create a record via the GraphQL API, then upload the files to Storage, and finally add the associations between the record and files. -```ts title="src/App.tsx" + +```swift title="ContentView" +import Amplify + +// Create the API record: +func createPhotoAlbum() async -> PhotoAlbum? { + var photoAlbum: PhotoAlbum? + + do { + let photoAlbumData = PhotoAlbum(name: "My first PhotoAlbum") + let response = try await Amplify.API.mutate(request: .create(photoAlbumData)) + print("Created PhotoAlbum: ", response) + switch response { + case .success(let newPhotoAlbum): + photoAlbum = newPhotoAlbum + case .failure(let error): + print("Failed with result: \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to create PhotoAlbum: ", error) + + } catch { + print("Unexpected error: \(error)") + } + + return photoAlbum +} + +// Upload all files to Storage: +func uploadImages(images: [Data]) async throws { + if let album = await createPhotoAlbum() { + var imagePaths: [String] = [] + var signedUrls: [String] = [] + + for (index, image) in images.enumerated() { + let imagePath = "images/\(album.id)/photo-\(index)" + imagePaths.append(imagePath) + do { + let uploadTask = Amplify.Storage.uploadData(path: .fromString(imagePath), data: image) + let key = try await uploadTask.value + + let signedUrl = try await Amplify.Storage.getURL(key: key) + signedUrls.append(signedUrl.absoluteString) + } catch { + print("Error uploading image \(index): \(error)") + throw error // Re-throw the error for caller handling + } + + } + + await updatePhotoAlbumWithPhotos(photoAlbum: album, imagePaths: imagePaths) + + print(signedUrls) + } else { + print("Failed to create PhotoAlbum") + } +} + +// Update the record with the file associations: +func updatePhotoAlbumWithPhotos(photoAlbum: PhotoAlbum, imagePaths: [String]) async { + print("Inside UpdatePhotoAlbum imagePaths:", imagePaths) + let updatedPhotoAlbum = PhotoAlbum(id: photoAlbum.id, name: photoAlbum.name, imagePaths: imagePaths) + do { + let result = try await Amplify.API.mutate(request: .update(updatedPhotoAlbum)) + + switch result { + case .success(let updatedPhotoAlbum): + print("Update PhotoAlbum: ", updatedPhotoAlbum) + case .failure(let error): + print("Got failed result with \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to create song: ", error) + } catch { + print("Unexpected error: \(error)") + } +} +``` + + + +```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { uploadData, getUrl } from "aws-amplify/storage"; import type { Schema } from "../amplify/data/resource"; @@ -559,13 +729,84 @@ const signedUrls = await Promise.all( async (path) => await getUrl({ path: path! }) ) ); - ``` + ### Add new files to an associated record To associate additional files with a record, update the record with the keys returned by the Storage uploads. + +```swift title="ContentView" +func addNewFilesToAssociatedRecord(id: String, images: [Data]) async { + // Query existing record to retrieve currently associated files: + do { + let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: id)) + + switch queryResponse { + case .success(let album): + // Upload all files to Storage: + let newImagePaths = try await uploadNewImages(for: album!, images: images) + + // Merge existing and new file paths: + let updatedImagePaths = (album?.imagePaths)! + newImagePaths + + print("update image paths:", updatedImagePaths) + + do { + print("PhotoAlbum", album!) + + // Update record with merged file associations: + let result = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: album!.id, name: album!.name, imagePaths: updatedImagePaths))) + + print("Updated PhotoAlbum", result) + } + + case .failure(let error): + print("Get failed result with \(error.errorDescription)") + } + + } catch let error as APIError { + print("Failed to query photo album: ", error) + } catch { + print("Unexpected error: \(error)") + } + } + + func uploadNewImages(for album: PhotoAlbum, images: [Data]) async throws -> [String] { + var newImagePaths: [String] = [] + + for (index, image) in images.enumerated() { + var newIndex = index + if let albumImagePaths = album.imagePaths, !albumImagePaths.isEmpty { + newIndex = index + albumImagePaths.count + } + + let imagePath = "images/\(album.id)/photo-\(newIndex)" + + do { + let uploadTask = Amplify.Storage.uploadData(path: .fromString(imagePath), data: image) + let key = try await uploadTask.value + + print("new image key", key) + + newImagePaths.append(imagePath) + } catch { + print("Error uploading image \(index): \(error)") + throw error // Re-throw the error for caller handling + } + } + + print("new image paths:", newImagePaths) + + return newImagePaths + } +``` + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; @@ -621,13 +862,80 @@ const signedUrls = await Promise.all( async (path) => await getUrl({ path: path! }) ) ); - ``` + ### Update the file for an associated record Updating a file for an associated record is the same as updating a file for a single file record, with the exception that you will need to update the list of file keys. - + +```swift title="ContentView" +func updateFileForAssociatedRecord(currentPhotoAlbum: PhotoAlbum, newImage: Data, fileName: String) async { + do { + // Upload the new file to Storage + let newFilePath = "images/\(currentPhotoAlbum.id)-\(fileName)" + let uploadTask = Amplify.Storage.uploadData(path: .fromString(newFilePath), data: newImage) + let result = try await uploadTask.value + print("Uploaded new file: \(result)") + + // Query the existing record + let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) + + switch queryResponse { + case .success(let album): + // Retrieve the last image path + let lastImagePath = album?.imagePaths?.last + + // Remove the last file association by key + var updatedImagePaths = album?.imagePaths?.filter { $0 != lastImagePath } ?? [] + updatedImagePaths.append(result) + + // Update the record with the updated list of file paths + let updatedAlbum = PhotoAlbum(id: currentPhotoAlbum.id, name: currentPhotoAlbum.name, imagePaths: updatedImagePaths) + let updateResult = try await Amplify.API.mutate(request: .update(updatedAlbum)) + + switch updateResult { + case .success(let updatedAlbum): + print("Updated PhotoAlbum with new file: \(updatedAlbum)") + + // Retrieve signed URLs for the updated image paths + if let imagePaths = updatedAlbum.imagePaths { + let signedUrls = try await withThrowingTaskGroup(of: String?.self) { group in + for imagePath in imagePaths { + group.addTask { + try await Amplify.Storage.getURL(key: imagePath!).absoluteString + } + } + + var signedUrlsArray: [String] = [] + for try await signedUrl in group { + if let signedUrl = signedUrl { + signedUrlsArray.append(signedUrl) + } + } + return signedUrlsArray + } + print("Signed URLs: \(signedUrls)") + } + + case .failure(let error): + print("Failed to update PhotoAlbum: \(error.errorDescription)") + } + + case .failure(let error): + print("Failed to query PhotoAlbum: \(error.errorDescription)") + } + } catch { + print("Unexpected error: \(error)") + } +} +``` + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { uploadData, getUrl } from "aws-amplify/storage"; @@ -686,11 +994,56 @@ const signedUrls = await Promise.all( ); ``` - + ### Query a record and retrieve the associated files To retrieve the files associated with a record, first query the record, then use Storage to retrieve all of the signed URLs. + +```swift title="ContentView" +func getImagesForPhotoAlbum(photoAlbumId: String) async throws { + do { + // Query the record to get the file paths + let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: photoAlbumId)) + + switch queryResponse { + case .success(let photoAlbum): + // If the record has no associated files, return early + guard let imagePaths = photoAlbum?.imagePaths, !imagePaths.isEmpty else { + return + } + + // Retrieve the signed URLs for the associated images + let signedUrls = try await withThrowingTaskGroup(of: URL?.self) { group in + for imagePath in imagePaths { + group.addTask { + try await Amplify.Storage.getURL(key: imagePath!) + } + } + + var signedUrlsArray: [URL] = [] + for try await signedUrl in group { + if let signedUrl = signedUrl { + signedUrlsArray.append(signedUrl) + } + } + return signedUrlsArray + } + + print(signedUrls) + + case .failure(let error): + throw error + } + } catch { + throw error + } +} +``` + + ```ts title="src/App.tsx" async function getImagesForPhotoAlbum() { import { generateClient } from "aws-amplify/api"; @@ -706,6 +1059,7 @@ const client = generateClient({ const response = await client.models.PhotoAlbum.get({ id: currentPhotoAlbum.id, }); + const photoAlbum = response.data; // If the record has no associated files, we can return early. @@ -718,8 +1072,9 @@ const signedUrls = await Promise.all( return await getUrl({ path: imagePath }); }) ); - +} ``` + ### Delete and remove files associated with API records @@ -727,6 +1082,40 @@ The workflow for deleting and removing files associated with API records is the #### Remove the file association, continue to persist both files and record + +```swift title="ContentView" +func removePhotoAssociation(currentPhotoAlbum: PhotoAlbum) async { + do { + let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) + + switch queryResponse { + case .success(let photoAlbum): + // If the record has no associated file, we can return early. + if photoAlbum?.imagePaths == nil || photoAlbum?.imagePaths?.isEmpty == true { + return + } + + // Update record with null imagePaths + let updatedPhotoAlbum = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: photoAlbum!.id, name: photoAlbum!.name, imagePaths: nil))) + + print("Updated PhotoAlbum: \(updatedPhotoAlbum)") + + case .failure(let error): + print("Failed to query PhotoAlbum: \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to remove photo association: ", error) + } catch { + print("Unexpected error: \(error)") + } +} +``` + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; @@ -750,11 +1139,56 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ id: photoAlbum.id, imagePaths: null, }); - ``` + #### Remove the record association and delete the files + +```swift title="ContentView" +func removePhotoAssociation(currentPhotoAlbum: PhotoAlbum) async { + do { + let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) + + switch queryResponse { + case .success(let photoAlbum): + // If the record has no associated file, we can return early. + if photoAlbum?.imagePaths == nil || photoAlbum?.imagePaths?.isEmpty == true { + return + } + + // Remove associated files from record + let updatedPhotoAlbum = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: photoAlbum!.id, name: photoAlbum!.name, imagePaths: nil))) + + print("Updated PhotoAlbum: \(updatedPhotoAlbum)") + + // Delete the files from S3 + if let imagePaths = photoAlbum?.imagePaths { + try await withThrowingTaskGroup(of: Void.self) { group in + for imagePath in imagePaths { + group.addTask { + try await Amplify.Storage.remove(key: imagePath!) + } + } + } + } + + case .failure(let error): + print("Failed to query PhotoAlbum: \(error.errorDescription)") + } + } catch let error as APIError { + print("Failed to remove photo association: ", error) + } catch { + print("Unexpected error: \(error)") + } +} +``` + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; import { remove } from "aws-amplify/storage"; @@ -789,11 +1223,40 @@ await Promise.all( await remove({ path: imagePath }); }) ); - ``` + #### Delete record and all associated files + +```swift title="ContentView" +import Amplify + +do { + let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) + + // If the record has no associated file, we can return early. + if (currentSong.coverArtPath == nil) { + return + } + + // Delete the record from the API + try await Amplify.API.mutate(request: .delete(Song.self, byId: currentSong.id)) + + // Delete the file from S3 + try await Amplify.Storage.remove(key: currentSong.coverArtPath!) + +} catch let error as APIError { + print("Failed to remove photo association: ", error) +} catch { + print("Unexpected error: \(error)") +} +``` + + + ```ts title="src/App.tsx" import { generateClient } from "aws-amplify/api"; @@ -830,6 +1293,7 @@ await Promise.all( ); ``` + ## Data consistency when working with records and files @@ -839,6 +1303,9 @@ One example is when we [create an API record, associate the Storage file with th It is important to understand when these mismatches can occur and to add meaningful error handling around these cases. This guide does not include exhaustive error handling, real-time subscriptions, re-querying of outdated records, or attempts to retry failed operations. However, these are all important considerations for a production-level application. + ## Complete examples @@ -1657,5 +2124,5 @@ export default withAuthenticator(App); ``` - + From 8703ad328db1f02f296e4b392f3c1a8426eb7dbc Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Tue, 11 Jun 2024 11:31:18 -0400 Subject: [PATCH 05/18] update swift examples and add complete example --- .../data/working-with-files/index.mdx | 1848 +++++++++++++---- 1 file changed, 1400 insertions(+), 448 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index df8b76ddeaf..f07931a383f 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -126,58 +126,23 @@ The API record's `id` is prepended to the Storage file name to ensure uniqueness ```swift title="ContentView" -import Amplify - - // Create the API record: -var song = Song(name: "My first song") +let song = Song(name: name) -do { - let result = try await Amplify.API.mutate(request: .create(song)) - - switch result { - case .success(let newSong): - song = newSong - case .failure(let error): - print("Got failed result with \(error.errorDescription)") - } -} catch let error as APIError { - print("Failed to create song: ", error) -} catch { - print("Unexpected error: \(error)") +guard let imageData = artCover.pngData() else { + print("Could not get data from UIImage.") + return } -// Upload the Storage file: -let uploadTask = Amplify.Storage.uploadData( - path: .fromString("images/\(song.id)-cover.png"), - data: imageData -) - -do { - let key = try await uploadTask.value - - // Add the file association to the record: - var updatedSong = Song(id: song.id, name: song.name, coverArtPath: key) +// Create the song record +var createdSong = try await Amplify.API.mutate(request: .create(song)).get() - let updateResult = try await Amplify.API.mutate(request: .update(Song(id: updatedSong.id, name: song.name, coverArtPath: nil))) +// Upload the art cover image +_ = Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)-cover.png"), data: imageData) - switch updateResult { - case .success(let song): - updatedSong = song - case .failure(let error): - print("Got failed result with \(error.errorDescription)") - } - - // If the record has no associated file, we can return early. - if (updatedSong.coverArtPath == nil) { - print("No associated file") - return - }; +// Update the song record with the image key +createdSong.coverArtPath = createdSong.id - // Retrieve the file's signed URL: - let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) - - print(signedUrl) -} +_ = try await Amplify.API.mutate(request: .update(createdSong)).get() ``` @@ -236,42 +201,18 @@ To associate a file with a record, update the record with the key returned by th ```swift title="ContentView" -import Amplify - -// Upload the Storage file: -let uploadTask = Amplify.Storage.uploadData( - path: .fromString("images/\(song.id)-cover.png"), - data: imageData -) +let imagePath = "images/\(currentSong.id)-cover.png" -let path = try await uploadTask.value +// Upload the new art image +_ = try await Amplify.Storage.uploadData(path: .fromString(imagePath), data: imageData).value // Add the file association to the record: -var updatedSong = Song(id: song.id, name: song.name, coverArtPath: path) - -do { - let updateResult = try await Amplify.API.mutate(request: .update(updatedSong)) +currentSong.coverArtPath = imagePath - switch updateResult { - case .success(let song): - updatedSong = song - case .failure(let error): - print("Got failed result with \(error.errorDescription)") - } -} catch let error as APIError { - print("Failed to create song: ", error) -} catch { - print("Unexpected error: \(error)") -} - -// If the record has no associated file, we can return early. -if (updatedSong.coverArtPath == nil) { - print("No associated file") - return -}; +let result = try await Amplify.API.mutate(request: .update(currentSong)).get() // Retrieve the file's signed URL: -let signedUrl = try await Amplify.Storage.getURL(key: updatedSong.coverArtPath!) +let signedUrl = try await Amplify.Storage.getURL(key: imagePath) ``` @@ -322,31 +263,13 @@ const signedURL = await getUrl({ path: updatedSong.coverArtPath }); To retrieve the file associated with a record, first query the record, then use Storage to get the signed URL. The signed URL can then be used to download the file, display an image, etc: ```swift title="ContentView" -import Amplify - -do { - let result = try await Amplify.API.query(request: .get(Song.self, byId: song.id)) +// Get the song record +let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() - switch result { - case .success(let song): +// Download the art cover +let imageData = try await Amplify.Storage.downloadData(path: .fromString((song?.coverArtPath)!)).value - // If the record has no associated file, we can return early. - if (song?.coverArtPath == nil) { - print("No associated file") - return - } else { - - // Retrieve the file's signed URL: - let signedUrl = try await Amplify.Storage.getURL(key: song!.coverArtPath!) - } - case .failure(let error): - print("Got failed result with \(error.errorDescription)") - } -} catch let error as APIError { - print("Failed to retrieve song: ", error) -} catch { - print("Unexpected error: \(error)") -} +let image = UIImage(data: imageData) ``` @@ -392,24 +315,19 @@ The following example removes the file association from the record, but does not ```swift title="ContentView" -import Amplify - -do { - let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) +// Get the song record +var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() - // If the record has no associated file, we can return early. - if (currentSong.coverArtPath == nil) { - return - } +// If the record has no associated file, we can return early. +guard song?.coverArtPath != nil else { + print("There is no cover art key to remove image association") + return +} - // Remove associated file from record - let updatedSong = try await Amplify.API.mutate(request: .update(Song(id: currentSong.id, name: currentSong.name, coverArtPath: nil))) +// Remove associated file from record +song?.coverArtPath = nil -} catch let error as APIError { - print("Failed to remove photo association: ", error) -} catch { - print("Unexpected error: \(error)") -} +let updateResult = try await Amplify.API.mutate(request: .update(song!)).get() ``` @@ -449,27 +367,22 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" -import Amplify - -do { - let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) +// Get the song record +var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() - // If the record has no associated file, we can return early. - if (currentSong.coverArtPath == nil) { - return - } +// If the record has no associated file, we can return early. +guard let coverArtPath = song?.coverArtPath else { + print("There is no cover art key to remove image association") + return +} - // Remove associated file from record - let updatedSong = try await Amplify.API.mutate(request: .update(Song(id: currentSong.id, name: currentSong.id, coverArtPath: nil))) +// Remove associated file from record +song?.coverArtPath = nil - // Delete the file from S3 - try await Amplify.Storage.remove(key: currentSong.coverArtPath!) +let updateResult = try await Amplify.API.mutate(request: .update(song!)).get() -} catch let error as APIError { - print("Failed to remove photo association: ", error) -} catch { - print("Unexpected error: \(error)") -} +// Delete the file from S3 +try await Amplify.Storage.remove(path: .fromString(coverArtPath)) ``` @@ -510,26 +423,15 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" -import Amplify - -do { - let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) +// Get the song record +let song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() - // If the record has no associated file, we can return early. - if (currentSong.coverArtPath == nil) { - return - } - - // Delete the record from the API - try await Amplify.API.mutate(request: .delete(Song.self, byId: currentSong.id)) +// Delete the record from the API +let deleteResult = try await Amplify.API.mutate(request: .delete(song!)) - // Delete the file from S3 - try await Amplify.Storage.remove(key: currentSong.coverArtPath!) - -} catch let error as APIError { - print("Failed to remove photo association: ", error) -} catch { - print("Unexpected error: \(error)") +// Delete the file from S3 +if let coverArt = song?.coverArtPath { + try await Amplify.Storage.remove(path: .fromString(coverArt)) } ``` @@ -591,82 +493,38 @@ First create a record via the GraphQL API, then upload the files to Storage, and ```swift title="ContentView" -import Amplify - // Create the API record: -func createPhotoAlbum() async -> PhotoAlbum? { - var photoAlbum: PhotoAlbum? - - do { - let photoAlbumData = PhotoAlbum(name: "My first PhotoAlbum") - let response = try await Amplify.API.mutate(request: .create(photoAlbumData)) - print("Created PhotoAlbum: ", response) - switch response { - case .success(let newPhotoAlbum): - photoAlbum = newPhotoAlbum - case .failure(let error): - print("Failed with result: \(error.errorDescription)") - } - } catch let error as APIError { - print("Failed to create PhotoAlbum: ", error) - - } catch { - print("Unexpected error: \(error)") - } +let album = PhotoAlbum(name: name) - return photoAlbum -} +var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get() // Upload all files to Storage: -func uploadImages(images: [Data]) async throws { - if let album = await createPhotoAlbum() { - var imagePaths: [String] = [] - var signedUrls: [String] = [] - - for (index, image) in images.enumerated() { - let imagePath = "images/\(album.id)/photo-\(index)" - imagePaths.append(imagePath) - - do { - let uploadTask = Amplify.Storage.uploadData(path: .fromString(imagePath), data: image) - let key = try await uploadTask.value - - let signedUrl = try await Amplify.Storage.getURL(key: key) - signedUrls.append(signedUrl.absoluteString) - } catch { - print("Error uploading image \(index): \(error)") - throw error // Re-throw the error for caller handling - } - - } - - await updatePhotoAlbumWithPhotos(photoAlbum: album, imagePaths: imagePaths) +let imagePaths = await withTaskGroup(of: String?.self) { group in + for imageData in imagesData { + group.addTask { + let path = "\(album.id)-\(UUID().uuidString)" + do { + _ = try await Amplify.Storage.uploadData(path: .fromString(path), + data: imageData + ).value + return path + } catch { + print("Failed with error:", error) + return nil + } + } + } - print(signedUrls) - } else { - print("Failed to create PhotoAlbum") - } + var imagePaths: [String?] = [] + for await imagePath in group { + imagePaths.append(imagePath) + } + return imagePaths.compactMap { $0 } } -// Update the record with the file associations: -func updatePhotoAlbumWithPhotos(photoAlbum: PhotoAlbum, imagePaths: [String]) async { - print("Inside UpdatePhotoAlbum imagePaths:", imagePaths) - let updatedPhotoAlbum = PhotoAlbum(id: photoAlbum.id, name: photoAlbum.name, imagePaths: imagePaths) - do { - let result = try await Amplify.API.mutate(request: .update(updatedPhotoAlbum)) - - switch result { - case .success(let updatedPhotoAlbum): - print("Update PhotoAlbum: ", updatedPhotoAlbum) - case .failure(let error): - print("Got failed result with \(error.errorDescription)") - } - } catch let error as APIError { - print("Failed to create song: ", error) - } catch { - print("Unexpected error: \(error)") - } -} +// Add the file association to the record: +createdAlbum.imagePaths = imagePaths +let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)).get() ``` @@ -734,73 +592,31 @@ const signedUrls = await Promise.all( ### Add new files to an associated record -To associate additional files with a record, update the record with the keys returned by the Storage uploads. +To associate additional files with a record, update the record with the paths returned by the Storage uploads. ```swift title="ContentView" -func addNewFilesToAssociatedRecord(id: String, images: [Data]) async { - // Query existing record to retrieve currently associated files: - do { - let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: id)) - - switch queryResponse { - case .success(let album): - // Upload all files to Storage: - let newImagePaths = try await uploadNewImages(for: album!, images: images) - - // Merge existing and new file paths: - let updatedImagePaths = (album?.imagePaths)! + newImagePaths - - print("update image paths:", updatedImagePaths) - - do { - print("PhotoAlbum", album!) - - // Update record with merged file associations: - let result = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: album!.id, name: album!.name, imagePaths: updatedImagePaths))) - - print("Updated PhotoAlbum", result) - } - - case .failure(let error): - print("Get failed result with \(error.errorDescription)") - } +// Upload file to Storage: +let path = "\(currentAlbum.id)-\(UUID().uuidString)" - } catch let error as APIError { - print("Failed to query photo album: ", error) - } catch { - print("Unexpected error: \(error)") - } - } - - func uploadNewImages(for album: PhotoAlbum, images: [Data]) async throws -> [String] { - var newImagePaths: [String] = [] +_ = try await Amplify.Storage.uploadData(path: .fromString(path), + data: imageData).value - for (index, image) in images.enumerated() { - var newIndex = index - if let albumImagePaths = album.imagePaths, !albumImagePaths.isEmpty { - newIndex = index + albumImagePaths.count - } - - let imagePath = "images/\(album.id)/photo-\(newIndex)" - - do { - let uploadTask = Amplify.Storage.uploadData(path: .fromString(imagePath), data: image) - let key = try await uploadTask.value +// Query existing record to retrieve currently associated files: +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() - print("new image key", key) +guard var imagePaths = album?.imagePaths else { + print("Album does not contain images") + return +} - newImagePaths.append(imagePath) - } catch { - print("Error uploading image \(index): \(error)") - throw error // Re-throw the error for caller handling - } - } +// Add new path to the existing paths +imagePaths.append(path) - print("new image paths:", newImagePaths) +// Update record with merged file associations: +album?.imagePaths = imagePaths - return newImagePaths - } +let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() ``` @@ -872,65 +688,24 @@ Updating a file for an associated record is the same as updating a file for a si "swift" ]}> ```swift title="ContentView" -func updateFileForAssociatedRecord(currentPhotoAlbum: PhotoAlbum, newImage: Data, fileName: String) async { - do { - // Upload the new file to Storage - let newFilePath = "images/\(currentPhotoAlbum.id)-\(fileName)" - let uploadTask = Amplify.Storage.uploadData(path: .fromString(newFilePath), data: newImage) - let result = try await uploadTask.value - print("Uploaded new file: \(result)") - - // Query the existing record - let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) - - switch queryResponse { - case .success(let album): - // Retrieve the last image path - let lastImagePath = album?.imagePaths?.last - - // Remove the last file association by key - var updatedImagePaths = album?.imagePaths?.filter { $0 != lastImagePath } ?? [] - updatedImagePaths.append(result) - - // Update the record with the updated list of file paths - let updatedAlbum = PhotoAlbum(id: currentPhotoAlbum.id, name: currentPhotoAlbum.name, imagePaths: updatedImagePaths) - let updateResult = try await Amplify.API.mutate(request: .update(updatedAlbum)) - - switch updateResult { - case .success(let updatedAlbum): - print("Updated PhotoAlbum with new file: \(updatedAlbum)") - - // Retrieve signed URLs for the updated image paths - if let imagePaths = updatedAlbum.imagePaths { - let signedUrls = try await withThrowingTaskGroup(of: String?.self) { group in - for imagePath in imagePaths { - group.addTask { - try await Amplify.Storage.getURL(key: imagePath!).absoluteString - } - } - - var signedUrlsArray: [String] = [] - for try await signedUrl in group { - if let signedUrl = signedUrl { - signedUrlsArray.append(signedUrl) - } - } - return signedUrlsArray - } - print("Signed URLs: \(signedUrls)") - } - - case .failure(let error): - print("Failed to update PhotoAlbum: \(error.errorDescription)") - } - - case .failure(let error): - print("Failed to query PhotoAlbum: \(error.errorDescription)") - } - } catch { - print("Unexpected error: \(error)") - } +// Upload new file to Storage: +let path = "\(currentAlbum.id)-\(UUID().uuidString)" + +_ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value + +// Update the album with the image keys +var album = currentAlbum + +if var imagePaths = album.imagePaths { + imagePaths.removeLast() + imagePaths.append(path) + album.imagePaths = imagePaths +} else { + album.imagePaths = [path] } + +// Update record with updated file associations: +let updateResult = try await Amplify.API.mutate(request: .update(album)).get() ``` ```swift title="ContentView" -func getImagesForPhotoAlbum(photoAlbumId: String) async throws { - do { - // Query the record to get the file paths - let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: photoAlbumId)) - - switch queryResponse { - case .success(let photoAlbum): - // If the record has no associated files, return early - guard let imagePaths = photoAlbum?.imagePaths, !imagePaths.isEmpty else { - return - } - - // Retrieve the signed URLs for the associated images - let signedUrls = try await withThrowingTaskGroup(of: URL?.self) { group in - for imagePath in imagePaths { - group.addTask { - try await Amplify.Storage.getURL(key: imagePath!) - } - } - - var signedUrlsArray: [URL] = [] - for try await signedUrl in group { - if let signedUrl = signedUrl { - signedUrlsArray.append(signedUrl) - } - } - return signedUrlsArray - } - - print(signedUrls) - - case .failure(let error): - throw error - } - } catch { - throw error - } +// Query the record to get the file paths: +let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() + +// If the record has no associated files, we can return early. +guard let imagePathsOptional = album?.imagePaths else { + print("Album does not contain images") + return +} + +let imagePaths = imagePathsOptional.compactMap { $0 } + +// Download the files from S3: +let images = await withTaskGroup(of: UIImage?.self) { group in + for path in imagePaths { + group.addTask { + do { + let imageData = try await Amplify.Storage.downloadData(path: .fromString(path)).value + return UIImage(data: imageData) + } catch { + print("Failed with error:", error) + return nil + } + } + } + + var images: [UIImage?] = [] + for await image in group { + images.append(image) + } + return images.compactMap { $0 } } ``` @@ -1086,33 +854,22 @@ The workflow for deleting and removing files associated with API records is the "swift" ]}> ```swift title="ContentView" -func removePhotoAssociation(currentPhotoAlbum: PhotoAlbum) async { - do { - let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) - - switch queryResponse { - case .success(let photoAlbum): - // If the record has no associated file, we can return early. - if photoAlbum?.imagePaths == nil || photoAlbum?.imagePaths?.isEmpty == true { - return - } - - // Update record with null imagePaths - let updatedPhotoAlbum = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: photoAlbum!.id, name: photoAlbum!.name, imagePaths: nil))) - - print("Updated PhotoAlbum: \(updatedPhotoAlbum)") - - case .failure(let error): - print("Failed to query PhotoAlbum: \(error.errorDescription)") - } - } catch let error as APIError { - print("Failed to remove photo association: ", error) - } catch { - print("Unexpected error: \(error)") - } +// Get the API record: +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() + +// If the record has no associated file, we can return early. +guard let imagePaths = album?.imagePaths, !imagePaths.isEmpty else { + print("There are no images to remove association") + return } + +// Remove the file association from the record: +album?.imagePaths = nil + +let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() ``` + @@ -1148,41 +905,33 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ "swift" ]}> ```swift title="ContentView" -func removePhotoAssociation(currentPhotoAlbum: PhotoAlbum) async { - do { - let queryResponse = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentPhotoAlbum.id)) - - switch queryResponse { - case .success(let photoAlbum): - // If the record has no associated file, we can return early. - if photoAlbum?.imagePaths == nil || photoAlbum?.imagePaths?.isEmpty == true { - return - } - - // Remove associated files from record - let updatedPhotoAlbum = try await Amplify.API.mutate(request: .update(PhotoAlbum(id: photoAlbum!.id, name: photoAlbum!.name, imagePaths: nil))) - - print("Updated PhotoAlbum: \(updatedPhotoAlbum)") - - // Delete the files from S3 - if let imagePaths = photoAlbum?.imagePaths { - try await withThrowingTaskGroup(of: Void.self) { group in - for imagePath in imagePaths { - group.addTask { - try await Amplify.Storage.remove(key: imagePath!) - } - } - } - } - - case .failure(let error): - print("Failed to query PhotoAlbum: \(error.errorDescription)") - } - } catch let error as APIError { - print("Failed to remove photo association: ", error) - } catch { - print("Unexpected error: \(error)") - } +// Get the API record: +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() + +// If the record has no associated files, we can return early. +guard let imagePathsOptional = album?.imagePaths else { + print("Album does not contain images") + return +} + +let imagePaths = imagePathsOptional.compactMap { $0 } + +// Remove associated files from record: +album?.imagePaths = nil + +let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() + +// Delete the file from S3 +await withTaskGroup(of: Void.self) { group in + for path in imagePaths { + group.addTask { + do { + try await Amplify.Storage.remove(path: .fromString(path)) + } catch { + print("Failed with error:", error) + } + } + } } ``` @@ -1230,27 +979,34 @@ await Promise.all( ```swift title="ContentView" -import Amplify +// Get the album record +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() -do { - let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) - - // If the record has no associated file, we can return early. - if (currentSong.coverArtPath == nil) { - return - } +// If the record has no associated file, we can return early. +guard let imagePathsOptional = album?.imagePaths else { + print("Album does not contain images") - // Delete the record from the API - try await Amplify.API.mutate(request: .delete(Song.self, byId: currentSong.id)) + let deleteResult = try await Amplify.API.mutate(request: .delete(album!)).get() + return +} - // Delete the file from S3 - try await Amplify.Storage.remove(key: currentSong.coverArtPath!) +let imagePaths = imagePathsOptional.compactMap { $0 } -} catch let error as APIError { - print("Failed to remove photo association: ", error) -} catch { - print("Unexpected error: \(error)") +// Remove the files from S3: +await withTaskGroup(of: Void.self) { group in + for path in imagePaths { + group.addTask { + do { + try await Amplify.Storage.remove(path: .fromString(path)) + } catch { + print("Failed with error:", error) + } + } + } } + +// Delete the API record: +let deleteResult = try await Amplify.API.mutate(request: .delete(album!)).get() ``` @@ -1303,11 +1059,1207 @@ One example is when we [create an API record, associate the Storage file with th It is important to understand when these mismatches can occur and to add meaningful error handling around these cases. This guide does not include exhaustive error handling, real-time subscriptions, re-querying of outdated records, or attempts to retry failed operations. However, these are all important considerations for a production-level application. +## Complete examples -## Complete examples + + +```swift title="AmplifySwiftApp" +import SwiftUI +import Amplify +import AWSAPIPlugin +import AWSCognitoAuthPlugin +import AWSS3StoragePlugin +import Authenticator +import PhotosUI + +@main +struct WorkingWithFilesApp: App { + + init() { + do { + Amplify.Logging.logLevel = .verbose + try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.add(plugin: AWSS3StoragePlugin()) + try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) + try Amplify.configure() + print("Amplify configured with API, Storage, and Auth plugins!") + } catch { + print("Failed to initialize Amplify with \(error)") + } + } + + var body: some Scene { + WindowGroup { + Authenticator { state in + TabView { + SongView() + .tabItem { + Label("Song", systemImage: "music.note") + } + + PhotoAlbumView() + .tabItem { + Label("PhotoAlbum", systemImage: "photo") + } + } + + } + } + } +} + +struct SignOutButton: View { + var body: some View { + Button("Sign out") { + Task { + await Amplify.Auth.signOut() + } + }.foregroundColor(.black) + } +} + +struct TappedButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(10) + .background(configuration.isPressed ? Color.teal.opacity(0.8) : Color.teal) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} + +extension Color { + static let teal = Color(red: 45/255, green: 111/255, blue: 138/255) +} + +struct DimmedBackgroundView: View { + var body: some View { + Color.gray.opacity(0.5) + .ignoresSafeArea() + } +} + +struct ImagePicker: UIViewControllerRepresentable { + @Binding var selectedImage: UIImage? + @Environment(\.presentationMode) var presentationMode + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + if let uiImage = info[.originalImage] as? UIImage { + parent.selectedImage = uiImage + } + parent.presentationMode.wrappedValue.dismiss() + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + parent.presentationMode.wrappedValue.dismiss() + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + let imagePicker = UIImagePickerController() + imagePicker.delegate = context.coordinator + return imagePicker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { + } +} + +struct MultiImagePicker: UIViewControllerRepresentable { + @Binding var selectedImages: [UIImage] + + func makeUIViewController(context: Context) -> PHPickerViewController { + var configuration = PHPickerConfiguration() + configuration.filter = .images + configuration.selectionLimit = 0 + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { + // No need for updates in this case + } + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + class Coordinator: PHPickerViewControllerDelegate { + private let parent: MultiImagePicker + + init(parent: MultiImagePicker) { + self.parent = parent + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true, completion: nil) + DispatchQueue.main.async { + self.parent.selectedImages = [] + } + for result in results { + if result.itemProvider.canLoadObject(ofClass: UIImage.self) { + result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in + if let image = image as? UIImage { + DispatchQueue.main.async { + self.parent.selectedImages.append(image) + } + } + } + } + } + } + } +} +``` + + +```swift title="SongView" +import SwiftUI +import Amplify + +class SongViewModel: ObservableObject { + + @Published var currentSong: Song? = nil + @Published var currentImage: UIImage? = nil + @Published var isLoading: Bool = false + + // Create a song with an associated image + func createSong(name: String, artCover: UIImage) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + let song = Song(name: name) + + guard let imageData = artCover.pngData() else { + print("Could not get data from image.") + return + } + + // Create the song record + let result = try await Amplify.API.mutate(request: .create(song)) + guard case .success(var createdSong) = result else { + print("Failed with error: ", result) + return + } + + // Upload the art cover image + _ = try await Amplify.Storage.uploadData(key: createdSong.id, + data: imageData, + options: .init(accessLevel: .private)).value + + // Update the song record with the image key + createdSong.coverArtKey = createdSong.id + let updateResult = try await Amplify.API.mutate(request: .update(createdSong)) + guard case .success(let updatedSong) = updateResult else { + print("Failed with error: ", updateResult) + return + } + + await setCurrentSong(updatedSong) + } + + // Add or update an image for an associated record + func updateArtCover(artCover: UIImage) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard var currentSong = currentSong else { + print("There is no song to associated the image with. Create a Song first.") + return + } + guard let imageData = artCover.pngData() else { + print("Could not get data from UIImage.") + return + } + + // Upload the new art image + _ = try await Amplify.Storage.uploadData(key: currentSong.id, + data: imageData, + options: .init(accessLevel: .private)).value + + // Update the song record + currentSong.coverArtKey = currentSong.id + let result = try await Amplify.API.mutate(request: .update(currentSong)) + guard case .success(let updatedSong) = result else { + print("Failed with error: ", result) + return + } + + await setCurrentSong(updatedSong) + } + + func refreshSongAndArtCover() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentSong = currentSong else { + print("There is no song to refresh the record and image. Create a song first.") + return + } + await setCurrentSong(nil) + await setCurrentImage(nil) + // Get the song record + let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) + guard case .success(let queriedSong) = result else { + print("Failed with error: ", result) + return + } + guard let song = queriedSong else { + print("Song may have been deleted, no song with id: ", currentSong.id) + await setCurrentSong(nil) + return + } + + guard let coverArtKey = song.coverArtKey else { + print("Song does not contain cover art") + await setCurrentSong(song) + await setCurrentImage(nil) + return + } + + // Download the art cover + let imageData = try await Amplify.Storage.downloadData(key: coverArtKey, + options: .init(accessLevel: .private)).value + let image = UIImage(data: imageData) + await setCurrentSong(song) + await setCurrentImage(image) + } + + func removeImageAssociationFromSong() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentSong = currentSong else { + print("There is no song to remove art cover from it. Create a song first.") + return + } + + // Get the song record + let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) + guard case .success(let queriedSong) = result else { + print("Failed with error: ", result) + return + } + guard var song = queriedSong else { + print("Song may have been deleted, no song by id: ", currentSong.id) + await setCurrentSong(nil) + return + } + guard song.coverArtKey != nil else { + print("There is no cover art key to remove image association") + return + } + + // Set the association to nil and update it + song.coverArtKey = nil + let updateResult = try await Amplify.API.mutate(request: .update(song)) + guard case .success(let updatedSong) = updateResult else { + print("Failed with error: ", result) + return + } + + await setCurrentSong(updatedSong) + } + + func removeImageAssociationAndDeleteImage() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentSong = currentSong else { + print("There is no song to remove art cover from it. Create a song first.") + return + } + + // Get the song record + let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) + guard case .success(let queriedSong) = result else { + print("Failed with error: ", result) + return + } + guard var song = queriedSong else { + print("Song may have been deleted, no song by id: ", currentSong.id) + await setCurrentSong(nil) + return + } + guard let coverArtKey = song.coverArtKey else { + print("There is no cover art key to remove image association") + return + } + + // Set the association to nil and update it + song.coverArtKey = nil + let updateResult = try await Amplify.API.mutate(request: .update(song)) + guard case .success(let updatedSong) = updateResult else { + print("Failed with error: ", result) + return + } + + // Remove the image + try await Amplify.Storage.remove(key: coverArtKey, + options: .init(accessLevel: .private)) + + await setCurrentSong(updatedSong) + await setCurrentImage(nil) + } + + func deleteSongAndArtCover() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentSong = currentSong else { + print("There is no song to delete. Create a song first.") + return + } + + // Get the song record + let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) + guard case .success(let queriedSong) = result else { + print("Failed with error: ", result) + return + } + guard let song = queriedSong else { + print("Song may have been deleted, no song by id: ", currentSong.id) + await setCurrentSong(nil) + return + } + + if let coverArt = song.coverArtKey { + // Remove the image + try await Amplify.Storage.remove(key: coverArt, + options: .init(accessLevel: .private)) + } + + // Delete the song record + let deleteResult = try await Amplify.API.mutate(request: .delete(song)) + guard case .success = deleteResult else { + print("Failed with error: ", deleteResult) + return + } + await setCurrentSong(nil) + await setCurrentImage(nil) + } + + @MainActor + func setCurrentSong(_ song: Song?) { + self.currentSong = song + } + + @MainActor + func setCurrentImage(_ image: UIImage?) { + self.currentImage = image + } + + @MainActor + func setIsLoading(_ isLoading: Bool) { + self.isLoading = isLoading + } +} + +struct SongView: View { + + @State private var isImagePickerPresented = false + @State private var songName: String = "" + + @StateObject var viewModel = SongViewModel() + + var body: some View { + NavigationView { + ZStack { + VStack { + SongInformation() + DisplayImage() + OpenImagePickerButton() + SongNameTextField() + CreateOrUpdateSongButton() + AdditionalOperations() + Spacer() + } + .padding() + .sheet(isPresented: $isImagePickerPresented) { + ImagePicker(selectedImage: $viewModel.currentImage) + } + VStack { + IsLoadingView() + } + } + .navigationBarItems(trailing: SignOutButton()) + } + } + + @ViewBuilder + func SongInformation() -> some View { + if let song = viewModel.currentSong { + Text("Song Id: \(song.id)").font(.caption) + if song.name != "" { + Text("Song Name: \(song.name)").font(.caption) + } + } + } + + @ViewBuilder + func DisplayImage() -> some View { + if let image = viewModel.currentImage { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + } else { + Text("No Image Selected") + .foregroundColor(.gray) + } + + } + + func OpenImagePickerButton() -> some View { + Button("Select \(viewModel.currentImage != nil ? "a new ": "" )song album cover") { + isImagePickerPresented.toggle() + }.buttonStyle(TappedButtonStyle()) + } + + @ViewBuilder + func SongNameTextField() -> some View { + TextField("\(viewModel.currentSong != nil ? "Update": "Enter") song name", text: $songName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .multilineTextAlignment(.center) + } + + @ViewBuilder + func CreateOrUpdateSongButton() -> some View { + if viewModel.currentSong == nil, let image = viewModel.currentImage { + Button("Save") { + Task { + try? await viewModel.createSong(name: songName, + artCover: image) + } + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + } else if viewModel.currentSong != nil, let image = viewModel.currentImage { + Button("Update") { + Task { + try? await viewModel.updateArtCover(artCover: image) + } + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + } + } + + @ViewBuilder + func AdditionalOperations() -> some View { + if viewModel.currentSong != nil { + VStack { + Button("Refresh") { + Task { + try? await viewModel.refreshSongAndArtCover() + } + }.buttonStyle(TappedButtonStyle()) + Button("Remove association from song") { + Task { + try? await viewModel.removeImageAssociationFromSong() + } + }.buttonStyle(TappedButtonStyle()) + Button("Remove association and delete image") { + Task { + try? await viewModel.removeImageAssociationAndDeleteImage() + } + }.buttonStyle(TappedButtonStyle()) + Button("Delete song and art cover") { + Task { + try? await viewModel.deleteSongAndArtCover() + } + songName = "" + }.buttonStyle(TappedButtonStyle()) + }.disabled(viewModel.isLoading) + } + } + + @ViewBuilder + func IsLoadingView() -> some View { + if viewModel.isLoading { + ZStack { + DimmedBackgroundView() + ProgressView() + } + } + } +} + +struct SongView_Previews: PreviewProvider { + static var previews: some View { + SongView() + } +} +``` + + +```swift title="PhotoAlbumView" +import SwiftUI +import Amplify +import Photos + +class PhotoAlbumViewModel: ObservableObject { + @Published var currentImages: [UIImage] = [] + @Published var currentAlbum: PhotoAlbum? = nil + @Published var isLoading: Bool = false + + // Create a record with multiple associated files + func createPhotoAlbum(name: String, photos: [UIImage]) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + let imagesData = photos.compactMap { $0.pngData() } + guard !imagesData.isEmpty else { + print("Could not get data from [UIImage]") + return + } + + // Create the photo album record + let album = PhotoAlbum(name: name) + let result = try await Amplify.API.mutate(request: .create(album)) + guard case .success(var createdAlbum) = result else { + print("Failed with error: ", result) + return + } + + // Upload the photo album images + let imageKeys = await withTaskGroup(of: String?.self) { group in + for imageData in imagesData { + group.addTask { + let key = "\(album.id)-\(UUID().uuidString)" + do { + _ = try await Amplify.Storage.uploadData(key: key, + data: imageData, + options: .init(accessLevel: .private)).value + return key + } catch { + print("Failed with error:", error) + return nil + } + } + } + + var imageKeys: [String?] = [] + for await imageKey in group { + imageKeys.append(imageKey) + } + return imageKeys.compactMap { $0 } + } + + // Update the album with the image keys + createdAlbum.imageKeys = imageKeys + let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", updateResult) + return + } + + await setCurrentAlbum(updatedAlbum) + } + + // Create a record with a single associated file + func createPhotoAlbum(name: String, photo: UIImage) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard let imageData = photo.pngData() else { + print("Could not get data from UIImage") + return + } + + // Create the photo album record + let album = PhotoAlbum(name: name) + let result = try await Amplify.API.mutate(request: .create(album)) + guard case .success(var createdAlbum) = result else { + print("Failed with error: ", result) + return + } + + // Upload the photo album image + let key = "\(album.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(key: key, + data: imageData, + options: .init(accessLevel: .private)).value + + // Update the album with the image key + createdAlbum.imageKeys = [key] + let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", updateResult) + return + } + + await setCurrentAlbum(updatedAlbum) + } + + // Add new file to an associated record + func addAdditionalPhotos(_ photo: UIImage) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + + guard let imageData = photo.pngData() else { + print("Could not get data from UIImage.") + return + } + + // Upload the new photo album image + let key = "\(currentAlbum.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(key: key, + data: imageData, + options: .init(accessLevel: .private)).value + + // Get the latest album + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + guard case .success(let queriedAlbum) = result else { + print("Failed with error: ", result) + return + } + guard var album = queriedAlbum else { + print("Album may have been deleted, no album with id: ", currentAlbum.id) + await setCurrentAlbum(nil) + return + } + + guard var imageKeys = album.imageKeys else { + print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) + return + } + + // Add new to the existing keys + imageKeys.append(key) + + // Update the album with the image keys + album.imageKeys = imageKeys + let updateResult = try await Amplify.API.mutate(request: .update(album)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", updateResult) + return + } + + await setCurrentAlbum(updatedAlbum) + } + + func replaceLastImage(_ photo: UIImage) async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + + guard let imageData = photo.pngData() else { + print("Could not get data from UIImage") + return + } + + + // Upload the new photo album image + let key = "\(currentAlbum.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(key: key, + data: imageData, + options: .init(accessLevel: .private)).value + + // Update the album with the image keys + var album = currentAlbum + if var imageKeys = album.imageKeys { + imageKeys.removeLast() + imageKeys.append(key) + album.imageKeys = imageKeys + } else { + album.imageKeys = [key] + } + let updateResult = try await Amplify.API.mutate(request: .update(album)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", updateResult) + return + } + + await setCurrentAlbum(updatedAlbum) + } + + // Query a record and retrieve the associated files + func refreshAlbumAndPhotos() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + await setCurrentAlbum(nil) + await setCurrentImages([]) + + // Get the song record + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + guard case .success(let queriedAlbum) = result else { + print("Failed with error: ", result) + return + } + guard let album = queriedAlbum else { + print("Album may have been deleted, no album with id: ", currentAlbum.id) + await setCurrentAlbum(nil) + return + } + + guard let imageKeysOptional = album.imageKeys else { + print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) + return + } + let imageKeys = imageKeysOptional.compactMap { $0 } + + // Download the photos + let images = await withTaskGroup(of: UIImage?.self) { group in + for key in imageKeys { + group.addTask { + do { + let imageData = try await Amplify.Storage.downloadData(key: key, + options: .init(accessLevel: .private)).value + return UIImage(data: imageData) + } catch { + print("Failed with error:", error) + return nil + } + } + } + + var images: [UIImage?] = [] + for await image in group { + images.append(image) + } + return images.compactMap { $0 } + } + + await setCurrentAlbum(album) + await setCurrentImages(images) + } + + // Remove the file association + func removeStorageAssociationsFromAlbum() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + + // Get the album record + let result = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + guard case .success(let queriedAlbum) = result else { + print("Failed with error: ", result) + return + } + guard var album = queriedAlbum else { + print("Song may have been deleted, no song by id: ", currentAlbum.id) + await setCurrentAlbum(nil) + return + } + guard let imageKeys = album.imageKeys, !imageKeys.isEmpty else { + print("There are no images to remove association") + return + } + + // Set the association to nil and update it + album.imageKeys = nil + let updateResult = try await Amplify.API.mutate(request: .update(album)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", result) + return + } + + await setCurrentAlbum(updatedAlbum) + } + + // Remove the record association and delete the files + func removeStorageAssociationsAndDeletePhotos() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + + // Get the album record + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + guard case .success(let queriedAlbum) = result else { + print("Failed with error: ", result) + return + } + guard var album = queriedAlbum else { + print("Album may have been deleted, no album with id: ", currentAlbum.id) + await setCurrentAlbum(nil) + return + } + + guard let imageKeysOptional = album.imageKeys else { + print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) + return + } + let imageKeys = imageKeysOptional.compactMap { $0 } + + // Set the associations to nil and update it + album.imageKeys = nil + let updateResult = try await Amplify.API.mutate(request: .update(album)) + guard case .success(let updatedAlbum) = updateResult else { + print("Failed with error: ", result) + return + } + + // Remove the photos + await withTaskGroup(of: Void.self) { group in + for key in imageKeys { + group.addTask { + do { + try await Amplify.Storage.remove(key: key, + options: .init(accessLevel: .private)) + } catch { + print("Failed with error:", error) + } + } + } + + for await _ in group { + } + } + + await setCurrentAlbum(updatedAlbum) + await setCurrentImages([]) + } + + // Delete record and all associated files + func deleteAlbumAndPhotos() async throws { + await setIsLoading(true) + defer { + Task { + await setIsLoading(false) + } + } + + guard let currentAlbum = currentAlbum else { + print("There is no album to associated the images with. Create an Album first.") + return + } + + // Get the album record + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + guard case .success(let queriedAlbum) = result else { + print("Failed with error: ", result) + return + } + guard let album = queriedAlbum else { + print("Album may have been deleted, no album with id: ", currentAlbum.id) + await setCurrentAlbum(nil) + return + } + + guard let imageKeysOptional = album.imageKeys else { + print("Album does not contain images") + + // Delete the album record + let deleteResult = try await Amplify.API.mutate(request: .delete(album)) + guard case .success = deleteResult else { + print("Failed with error: ", deleteResult) + return + } + + await setCurrentAlbum(nil) + await setCurrentImages([]) + return + } + let imageKeys = imageKeysOptional.compactMap { $0 } + + // Remove the photos + await withTaskGroup(of: Void.self) { group in + for key in imageKeys { + group.addTask { + do { + try await Amplify.Storage.remove(key: key, + options: .init(accessLevel: .private)) + } catch { + print("Failed with error:", error) + } + } + } + + for await _ in group { + } + } + + // Delete the album record + let deleteResult = try await Amplify.API.mutate(request: .delete(album)) + guard case .success = deleteResult else { + print("Failed with error: ", deleteResult) + return + } + + await setCurrentAlbum(nil) + await setCurrentImages([]) + } + + @MainActor + func setCurrentAlbum(_ album: PhotoAlbum?) { + self.currentAlbum = album + } + + @MainActor + func setCurrentImages(_ images: [UIImage]) { + self.currentImages = images + } + + @MainActor + func setIsLoading(_ isLoading: Bool) { + self.isLoading = isLoading + } +} + +struct PhotoAlbumView: View { + @State private var isImagePickerPresented: Bool = false + @State private var albumName: String = "" + @State private var isLastImagePickerPresented = false + @State private var lastImage: UIImage? = nil + @StateObject var viewModel = PhotoAlbumViewModel() + + var body: some View { + NavigationView { + ZStack { + VStack { + AlbumInformation() + DisplayImages() + OpenImagePickerButton() + PhotoAlbumNameTextField() + CreateOrUpdateAlbumButton() + AdditionalOperations() + } + .padding() + .sheet(isPresented: $isImagePickerPresented) { + MultiImagePicker(selectedImages: $viewModel.currentImages) + } + .sheet(isPresented: $isLastImagePickerPresented) { + ImagePicker(selectedImage: $lastImage) + } + VStack { + IsLoadingView() + } + } + .navigationBarItems(trailing: SignOutButton()) + } + } + + @ViewBuilder + func AlbumInformation() -> some View { + if let album = viewModel.currentAlbum { + Text("Album Id: \(album.id)").font(.caption) + if album.name != "" { + Text("Album Name: \(album.name)").font(.caption) + } + } + } + + @ViewBuilder + func DisplayImages() -> some View { + // Display selected images + ScrollView(.horizontal) { + HStack { + ForEach($viewModel.currentImages, id: \.self) { image in + Image(uiImage: image.wrappedValue) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + } + } + } + if $viewModel.currentImages.isEmpty { + Text("No Images Selected") + .foregroundColor(.gray) + } + } + + func OpenImagePickerButton() -> some View { + // Button to open the image picker + Button("Select \(!viewModel.currentImages.isEmpty ? "new " : "")photo album images") { + isImagePickerPresented.toggle() + }.buttonStyle(TappedButtonStyle()) + } + + @ViewBuilder + func PhotoAlbumNameTextField() -> some View { + TextField("\(viewModel.currentAlbum != nil ? "Update": "Enter") album name", text: $albumName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .multilineTextAlignment(.center) + } + + @ViewBuilder + func CreateOrUpdateAlbumButton() -> some View { + if viewModel.currentAlbum == nil, !viewModel.currentImages.isEmpty { + Button("Save") { + Task { + try? await viewModel.createPhotoAlbum(name: albumName, + photos: viewModel.currentImages) + } + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + } else if viewModel.currentAlbum != nil { + Button("Select \(lastImage != nil ? "another ": "")photo to replace last photo in the album") { + isLastImagePickerPresented.toggle() + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + + if let lastImage = lastImage { + Image(uiImage: lastImage) + .resizable() + .aspectRatio(contentMode: .fit) + Button("Replace last image in album with above") { + Task { + try? await viewModel.replaceLastImage(lastImage) + self.lastImage = nil + try? await viewModel.refreshAlbumAndPhotos() + } + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + Button("Append above image to album") { + Task { + try? await viewModel.addAdditionalPhotos(lastImage) + self.lastImage = nil + try? await viewModel.refreshAlbumAndPhotos() + } + } + .buttonStyle(TappedButtonStyle()) + .disabled(viewModel.isLoading) + } + } + } + + @ViewBuilder + func AdditionalOperations() -> some View { + if viewModel.currentAlbum != nil { + VStack { + Button("Refresh") { + Task { + try? await viewModel.refreshAlbumAndPhotos() + } + }.buttonStyle(TappedButtonStyle()) + Button("Remove associations from album") { + Task { + try? await viewModel.removeStorageAssociationsFromAlbum() + try? await viewModel.refreshAlbumAndPhotos() + } + }.buttonStyle(TappedButtonStyle()) + Button("Remove association and delete photos") { + Task { + try? await viewModel.removeStorageAssociationsAndDeletePhotos() + try? await viewModel.refreshAlbumAndPhotos() + } + }.buttonStyle(TappedButtonStyle()) + Button("Delete album and images") { + Task { + try? await viewModel.deleteAlbumAndPhotos() + } + albumName = "" + }.buttonStyle(TappedButtonStyle()) + }.disabled(viewModel.isLoading) + } + } + + @ViewBuilder + func IsLoadingView() -> some View { + if viewModel.isLoading { + ZStack { + DimmedBackgroundView() + ProgressView() + } + } + } +} + +struct PhotoAlbumView_Previews: PreviewProvider { + static var previews: some View { + PhotoAlbumView() + } +} +``` + + + + + From 0d050288a44d10d84cce22ed28e709b978c42b3e Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Tue, 11 Jun 2024 11:39:43 -0400 Subject: [PATCH 06/18] replace byIdentifier with byId --- .../data/working-with-files/index.mdx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index f07931a383f..80e1f5feb66 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -264,7 +264,7 @@ To retrieve the file associated with a record, first query the record, then use ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() // Download the art cover let imageData = try await Amplify.Storage.downloadData(path: .fromString((song?.coverArtPath)!)).value @@ -316,7 +316,7 @@ The following example removes the file association from the record, but does not ```swift title="ContentView" // Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() // If the record has no associated file, we can return early. guard song?.coverArtPath != nil else { @@ -368,7 +368,7 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" // Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() // If the record has no associated file, we can return early. guard let coverArtPath = song?.coverArtPath else { @@ -424,7 +424,7 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() +let song = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)).get() // Delete the record from the API let deleteResult = try await Amplify.API.mutate(request: .delete(song!)) @@ -603,7 +603,7 @@ _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Query existing record to retrieve currently associated files: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() guard var imagePaths = album?.imagePaths else { print("Album does not contain images") @@ -777,7 +777,7 @@ To retrieve the files associated with a record, first query the record, then use ```swift title="ContentView" // Query the record to get the file paths: -let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() // If the record has no associated files, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -855,7 +855,7 @@ The workflow for deleting and removing files associated with API records is the ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() // If the record has no associated file, we can return early. guard let imagePaths = album?.imagePaths, !imagePaths.isEmpty else { @@ -906,7 +906,7 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() // If the record has no associated files, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -980,7 +980,7 @@ await Promise.all( ```swift title="ContentView" // Get the album record -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() // If the record has no associated file, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -1325,7 +1325,7 @@ class SongViewModel: ObservableObject { await setCurrentImage(nil) // Get the song record - let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) + let result = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) guard case .success(let queriedSong) = result else { print("Failed with error: ", result) return @@ -1364,7 +1364,7 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) + let result = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)) guard case .success(let queriedSong) = result else { print("Failed with error: ", result) return @@ -1403,7 +1403,7 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) + let result = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) guard case .success(let queriedSong) = result else { print("Failed with error: ", result) return @@ -1447,7 +1447,7 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) + let result = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)) guard case .success(let queriedSong) = result else { print("Failed with error: ", result) return @@ -1760,7 +1760,7 @@ class PhotoAlbumViewModel: ObservableObject { options: .init(accessLevel: .private)).value // Get the latest album - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) guard case .success(let queriedAlbum) = result else { print("Failed with error: ", result) return @@ -1851,7 +1851,7 @@ class PhotoAlbumViewModel: ObservableObject { await setCurrentImages([]) // Get the song record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) guard case .success(let queriedAlbum) = result else { print("Failed with error: ", result) return @@ -1910,7 +1910,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + let result = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) guard case .success(let queriedAlbum) = result else { print("Failed with error: ", result) return @@ -1951,7 +1951,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) guard case .success(let queriedAlbum) = result else { print("Failed with error: ", result) return @@ -2014,7 +2014,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) + let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) guard case .success(let queriedAlbum) = result else { print("Failed with error: ", result) return From 0f84d7111eda1aa958536c872622a6270605c970 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Thu, 13 Jun 2024 15:10:09 -0400 Subject: [PATCH 07/18] make code examples consistent, use guard and avoid force unwrapping, change byId to byIdentifier --- .../data/working-with-files/index.mdx | 378 ++++++------------ 1 file changed, 126 insertions(+), 252 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 80e1f5feb66..96dbdb1cf33 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -264,7 +264,7 @@ To retrieve the file associated with a record, first query the record, then use ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() +let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() // Download the art cover let imageData = try await Amplify.Storage.downloadData(path: .fromString((song?.coverArtPath)!)).value @@ -316,7 +316,7 @@ The following example removes the file association from the record, but does not ```swift title="ContentView" // Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() +var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() // If the record has no associated file, we can return early. guard song?.coverArtPath != nil else { @@ -368,7 +368,7 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" // Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() +var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() // If the record has no associated file, we can return early. guard let coverArtPath = song?.coverArtPath else { @@ -424,7 +424,7 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)).get() +let song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() // Delete the record from the API let deleteResult = try await Amplify.API.mutate(request: .delete(song!)) @@ -603,7 +603,7 @@ _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Query existing record to retrieve currently associated files: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() guard var imagePaths = album?.imagePaths else { print("Album does not contain images") @@ -777,7 +777,7 @@ To retrieve the files associated with a record, first query the record, then use ```swift title="ContentView" // Query the record to get the file paths: -let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() +let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() // If the record has no associated files, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -855,7 +855,7 @@ The workflow for deleting and removing files associated with API records is the ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() // If the record has no associated file, we can return early. guard let imagePaths = album?.imagePaths, !imagePaths.isEmpty else { @@ -906,7 +906,7 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() // If the record has no associated files, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -980,7 +980,7 @@ await Promise.all( ```swift title="ContentView" // Get the album record -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() +var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() // If the record has no associated file, we can return early. guard let imagePathsOptional = album?.imagePaths else { @@ -1078,15 +1078,16 @@ import PhotosUI struct WorkingWithFilesApp: App { init() { + _ = AWSAPIPlugin(modelRegistration: AmplifyModels()) do { Amplify.Logging.logLevel = .verbose try Amplify.add(plugin: AWSCognitoAuthPlugin()) + try Amplify.add(plugin: AWSAPIPlugin()) try Amplify.add(plugin: AWSS3StoragePlugin()) - try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) - try Amplify.configure() - print("Amplify configured with API, Storage, and Auth plugins!") + try Amplify.configure(with: .amplifyOutputs) + print("Amplify configured with Auth, API, and Storage plugins") } catch { - print("Failed to initialize Amplify with \(error)") + print("Unable to configure Amplify \(error)") } } @@ -1254,24 +1255,15 @@ class SongViewModel: ObservableObject { } // Create the song record - let result = try await Amplify.API.mutate(request: .create(song)) - guard case .success(var createdSong) = result else { - print("Failed with error: ", result) - return - } + var createdSong = try await Amplify.API.mutate(request: .create(song)).get() // Upload the art cover image - _ = try await Amplify.Storage.uploadData(key: createdSong.id, - data: imageData, - options: .init(accessLevel: .private)).value - - // Update the song record with the image key - createdSong.coverArtKey = createdSong.id - let updateResult = try await Amplify.API.mutate(request: .update(createdSong)) - guard case .success(let updatedSong) = updateResult else { - print("Failed with error: ", updateResult) - return - } + _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)"), + data: imageData).value + + // Update the song record with the image path + createdSong.coverArtPath = createdSong.id + let updatedSong = try await Amplify.API.mutate(request: .update(createdSong)).get() await setCurrentSong(updatedSong) } @@ -1295,17 +1287,12 @@ class SongViewModel: ObservableObject { } // Upload the new art image - _ = try await Amplify.Storage.uploadData(key: currentSong.id, - data: imageData, - options: .init(accessLevel: .private)).value + _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(currentSong.id)"), + data: imageData).value // Update the song record - currentSong.coverArtKey = currentSong.id - let result = try await Amplify.API.mutate(request: .update(currentSong)) - guard case .success(let updatedSong) = result else { - print("Failed with error: ", result) - return - } + currentSong.coverArtPath = currentSong.id + let updatedSong = try await Amplify.API.mutate(request: .update(currentSong)).get() await setCurrentSong(updatedSong) } @@ -1325,18 +1312,9 @@ class SongViewModel: ObservableObject { await setCurrentImage(nil) // Get the song record - let result = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) - guard case .success(let queriedSong) = result else { - print("Failed with error: ", result) - return - } - guard let song = queriedSong else { - print("Song may have been deleted, no song with id: ", currentSong.id) - await setCurrentSong(nil) - return - } + let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() - guard let coverArtKey = song.coverArtKey else { + guard let coverArtPath = song?.coverArtPath else { print("Song does not contain cover art") await setCurrentSong(song) await setCurrentImage(nil) @@ -1344,9 +1322,10 @@ class SongViewModel: ObservableObject { } // Download the art cover - let imageData = try await Amplify.Storage.downloadData(key: coverArtKey, - options: .init(accessLevel: .private)).value + let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value + let image = UIImage(data: imageData) + await setCurrentSong(song) await setCurrentImage(image) } @@ -1364,28 +1343,17 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)) - guard case .success(let queriedSong) = result else { - print("Failed with error: ", result) - return - } - guard var song = queriedSong else { - print("Song may have been deleted, no song by id: ", currentSong.id) - await setCurrentSong(nil) - return - } - guard song.coverArtKey != nil else { - print("There is no cover art key to remove image association") + var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() + + guard song?.coverArtPath != nil else { + print("There is no cover art path to remove image association") return } // Set the association to nil and update it - song.coverArtKey = nil - let updateResult = try await Amplify.API.mutate(request: .update(song)) - guard case .success(let updatedSong) = updateResult else { - print("Failed with error: ", result) - return - } + song?.coverArtPath = nil + + let updatedSong = try await Amplify.API.mutate(request: .update(song!)).get() await setCurrentSong(updatedSong) } @@ -1403,32 +1371,22 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)) - guard case .success(let queriedSong) = result else { - print("Failed with error: ", result) - return - } - guard var song = queriedSong else { - print("Song may have been deleted, no song by id: ", currentSong.id) - await setCurrentSong(nil) + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Error: could not retrieve the song") return } - guard let coverArtKey = song.coverArtKey else { - print("There is no cover art key to remove image association") + + guard let coverArtPath = song.coverArtPath else { + print("There is no cover art path to remove image association") return } // Set the association to nil and update it - song.coverArtKey = nil - let updateResult = try await Amplify.API.mutate(request: .update(song)) - guard case .success(let updatedSong) = updateResult else { - print("Failed with error: ", result) - return - } + song.coverArtPath = nil + let updatedSong = try await Amplify.API.mutate(request: .update(song)).get() // Remove the image - try await Amplify.Storage.remove(key: coverArtKey, - options: .init(accessLevel: .private)) + try await Amplify.Storage.remove(path: .fromString(coverArtPath)) await setCurrentSong(updatedSong) await setCurrentImage(nil) @@ -1447,29 +1405,19 @@ class SongViewModel: ObservableObject { } // Get the song record - let result = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)) - guard case .success(let queriedSong) = result else { - print("Failed with error: ", result) - return - } - guard let song = queriedSong else { - print("Song may have been deleted, no song by id: ", currentSong.id) - await setCurrentSong(nil) + guard var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Error: could not retrieve the song") return } - if let coverArt = song.coverArtKey { + if let coverArt = song.coverArtPath { // Remove the image - try await Amplify.Storage.remove(key: coverArt, - options: .init(accessLevel: .private)) + try await Amplify.Storage.remove(path: .fromString(coverArt)) } // Delete the song record - let deleteResult = try await Amplify.API.mutate(request: .delete(song)) - guard case .success = deleteResult else { - print("Failed with error: ", deleteResult) - return - } + _ = try await Amplify.API.mutate(request: .delete(song)).get() + await setCurrentSong(nil) await setCurrentImage(nil) } @@ -1563,7 +1511,7 @@ struct SongView: View { Button("Save") { Task { try? await viewModel.createSong(name: songName, - artCover: image) + artCover: image) } } .buttonStyle(TappedButtonStyle()) @@ -1654,22 +1602,16 @@ class PhotoAlbumViewModel: ObservableObject { // Create the photo album record let album = PhotoAlbum(name: name) - let result = try await Amplify.API.mutate(request: .create(album)) - guard case .success(var createdAlbum) = result else { - print("Failed with error: ", result) - return - } + var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get() // Upload the photo album images - let imageKeys = await withTaskGroup(of: String?.self) { group in + let imagePaths = await withTaskGroup(of: String?.self) { group in for imageData in imagesData { group.addTask { - let key = "\(album.id)-\(UUID().uuidString)" + let path = "images/\(album.id)-\(UUID().uuidString)" do { - _ = try await Amplify.Storage.uploadData(key: key, - data: imageData, - options: .init(accessLevel: .private)).value - return key + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value + return path } catch { print("Failed with error:", error) return nil @@ -1677,20 +1619,16 @@ class PhotoAlbumViewModel: ObservableObject { } } - var imageKeys: [String?] = [] - for await imageKey in group { - imageKeys.append(imageKey) + var imagePaths: [String?] = [] + for await imagePath in group { + imagePaths.append(imagePath) } - return imageKeys.compactMap { $0 } + return imagePaths.compactMap { $0 } } - // Update the album with the image keys - createdAlbum.imageKeys = imageKeys - let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", updateResult) - return - } + // Update the album with the image paths + createdAlbum.imagePaths = imagePaths + let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get() await setCurrentAlbum(updatedAlbum) } @@ -1711,25 +1649,15 @@ class PhotoAlbumViewModel: ObservableObject { // Create the photo album record let album = PhotoAlbum(name: name) - let result = try await Amplify.API.mutate(request: .create(album)) - guard case .success(var createdAlbum) = result else { - print("Failed with error: ", result) - return - } + var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get() // Upload the photo album image - let key = "\(album.id)-\(UUID().uuidString)" - _ = try await Amplify.Storage.uploadData(key: key, - data: imageData, - options: .init(accessLevel: .private)).value - - // Update the album with the image key - createdAlbum.imageKeys = [key] - let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", updateResult) - return - } + let path = "images/\(album.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value + + // Update the album with the image path + createdAlbum.imagePaths = [path] + let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get() await setCurrentAlbum(updatedAlbum) } @@ -1754,40 +1682,28 @@ class PhotoAlbumViewModel: ObservableObject { } // Upload the new photo album image - let key = "\(currentAlbum.id)-\(UUID().uuidString)" - _ = try await Amplify.Storage.uploadData(key: key, - data: imageData, - options: .init(accessLevel: .private)).value + let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Get the latest album - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) - guard case .success(let queriedAlbum) = result else { - print("Failed with error: ", result) - return - } - guard var album = queriedAlbum else { - print("Album may have been deleted, no album with id: ", currentAlbum.id) - await setCurrentAlbum(nil) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } - guard var imageKeys = album.imageKeys else { + guard var imagePaths = album.imagePaths else { print("Album does not contain images") await setCurrentAlbum(album) await setCurrentImages([]) return } - // Add new to the existing keys - imageKeys.append(key) + // Add new to the existing paths + imagePaths.append(path) - // Update the album with the image keys - album.imageKeys = imageKeys - let updateResult = try await Amplify.API.mutate(request: .update(album)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", updateResult) - return - } + // Update the album with the image paths + album.imagePaths = imagePaths + let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() await setCurrentAlbum(updatedAlbum) } @@ -1812,26 +1728,21 @@ class PhotoAlbumViewModel: ObservableObject { // Upload the new photo album image - let key = "\(currentAlbum.id)-\(UUID().uuidString)" - _ = try await Amplify.Storage.uploadData(key: key, - data: imageData, - options: .init(accessLevel: .private)).value + let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value - // Update the album with the image keys + // Update the album with the image paths var album = currentAlbum - if var imageKeys = album.imageKeys { - imageKeys.removeLast() - imageKeys.append(key) - album.imageKeys = imageKeys + if var imagePaths = album.imagePaths { + imagePaths.removeLast() + imagePaths.append(path) + album.imagePaths = imagePaths } else { - album.imageKeys = [key] - } - let updateResult = try await Amplify.API.mutate(request: .update(album)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", updateResult) - return + album.imagePaths = [path] } + let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() + await setCurrentAlbum(updatedAlbum) } @@ -1844,39 +1755,36 @@ class PhotoAlbumViewModel: ObservableObject { } } guard let currentAlbum = currentAlbum else { - print("There is no album to associated the images with. Create an Album first.") + print("There is no album to associate the images with. Create an Album first.") return } + await setCurrentAlbum(nil) await setCurrentImages([]) // Get the song record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) - guard case .success(let queriedAlbum) = result else { - print("Failed with error: ", result) - return - } - guard let album = queriedAlbum else { - print("Album may have been deleted, no album with id: ", currentAlbum.id) - await setCurrentAlbum(nil) + guard let album = try await Amplify.API.query( + request: + .get(PhotoAlbum.self, + byIdentifier: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } - guard let imageKeysOptional = album.imageKeys else { + guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") await setCurrentAlbum(album) await setCurrentImages([]) return } - let imageKeys = imageKeysOptional.compactMap { $0 } + let imagePaths = imagePathsOptional.compactMap { $0 } // Download the photos let images = await withTaskGroup(of: UIImage?.self) { group in - for key in imageKeys { + for path in imagePaths { group.addTask { do { - let imageData = try await Amplify.Storage.downloadData(key: key, - options: .init(accessLevel: .private)).value + let imageData = try await Amplify.Storage.downloadData(path: .fromString(path)).value return UIImage(data: imageData) } catch { print("Failed with error:", error) @@ -1910,28 +1818,19 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) - guard case .success(let queriedAlbum) = result else { - print("Failed with error: ", result) + guard var album = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } - guard var album = queriedAlbum else { - print("Song may have been deleted, no song by id: ", currentAlbum.id) - await setCurrentAlbum(nil) - return - } - guard let imageKeys = album.imageKeys, !imageKeys.isEmpty else { + + guard let imagePaths = album.imagePaths, !imagePaths.isEmpty else { print("There are no images to remove association") return } // Set the association to nil and update it - album.imageKeys = nil - let updateResult = try await Amplify.API.mutate(request: .update(album)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", result) - return - } + album.imagePaths = nil + let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() await setCurrentAlbum(updatedAlbum) } @@ -1951,40 +1850,29 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) - guard case .success(let queriedAlbum) = result else { - print("Failed with error: ", result) - return - } - guard var album = queriedAlbum else { - print("Album may have been deleted, no album with id: ", currentAlbum.id) - await setCurrentAlbum(nil) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } - guard let imageKeysOptional = album.imageKeys else { + guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") await setCurrentAlbum(album) await setCurrentImages([]) return } - let imageKeys = imageKeysOptional.compactMap { $0 } + let imagePaths = imagePathsOptional.compactMap { $0 } // Set the associations to nil and update it - album.imageKeys = nil - let updateResult = try await Amplify.API.mutate(request: .update(album)) - guard case .success(let updatedAlbum) = updateResult else { - print("Failed with error: ", result) - return - } + album.imagePaths = nil + let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() // Remove the photos await withTaskGroup(of: Void.self) { group in - for key in imageKeys { + for path in imagePaths { group.addTask { do { - try await Amplify.Storage.remove(key: key, - options: .init(accessLevel: .private)) + try await Amplify.Storage.remove(path: .fromString(path)) } catch { print("Failed with error:", error) } @@ -2014,40 +1902,30 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)) - guard case .success(let queriedAlbum) = result else { - print("Failed with error: ", result) - return - } - guard let album = queriedAlbum else { - print("Album may have been deleted, no album with id: ", currentAlbum.id) - await setCurrentAlbum(nil) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } - guard let imageKeysOptional = album.imageKeys else { + guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") // Delete the album record - let deleteResult = try await Amplify.API.mutate(request: .delete(album)) - guard case .success = deleteResult else { - print("Failed with error: ", deleteResult) - return - } + _ = try await Amplify.API.mutate(request: .delete(album)) await setCurrentAlbum(nil) await setCurrentImages([]) return } - let imageKeys = imageKeysOptional.compactMap { $0 } + + let imagePaths = imagePathsOptional.compactMap { $0 } // Remove the photos await withTaskGroup(of: Void.self) { group in - for key in imageKeys { + for path in imagePaths { group.addTask { do { - try await Amplify.Storage.remove(key: key, - options: .init(accessLevel: .private)) + try await Amplify.Storage.remove(path: .fromString(path)) } catch { print("Failed with error:", error) } @@ -2059,11 +1937,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Delete the album record - let deleteResult = try await Amplify.API.mutate(request: .delete(album)) - guard case .success = deleteResult else { - print("Failed with error: ", deleteResult) - return - } + _ = try await Amplify.API.mutate(request: .delete(album)).get() await setCurrentAlbum(nil) await setCurrentImages([]) @@ -2167,7 +2041,7 @@ struct PhotoAlbumView: View { Button("Save") { Task { try? await viewModel.createPhotoAlbum(name: albumName, - photos: viewModel.currentImages) + photos: viewModel.currentImages) } } .buttonStyle(TappedButtonStyle()) From 423cd518dc6070f689799088b387cfdcae8ed547 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 10:37:35 -0400 Subject: [PATCH 08/18] Update index.mdx Co-authored-by: Michael Law <1365977+lawmicha@users.noreply.github.com> --- .../build-a-backend/data/working-with-files/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 96dbdb1cf33..6f2b2a231de 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -1405,8 +1405,8 @@ class SongViewModel: ObservableObject { } // Get the song record - guard var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Error: could not retrieve the song") + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) return } From 9888930680edda21b64c59e5237f36ed613fd087 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 13:13:14 -0400 Subject: [PATCH 09/18] replace references of key with path --- .../data/working-with-files/index.mdx | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 96dbdb1cf33..b10d054f140 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -120,7 +120,7 @@ You can create a record via the Amplify Data client, upload a file to Storage, a -The API record's `id` is prepended to the Storage file name to ensure uniqueness. If this is excluded, multiple API records could then be associated with the same file key unintentionally. +The API record's `id` is prepended to the Storage file name to ensure uniqueness. If this is excluded, multiple API records could then be associated with the same file path unintentionally. @@ -139,7 +139,7 @@ var createdSong = try await Amplify.API.mutate(request: .create(song)).get() // Upload the art cover image _ = Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)-cover.png"), data: imageData) -// Update the song record with the image key +// Update the song record with the image path createdSong.coverArtPath = createdSong.id _ = try await Amplify.API.mutate(request: .update(createdSong)).get() @@ -197,7 +197,7 @@ const signedURL = await getUrl({ path: updatedSong.coverArtPath }); ## Add or update a file for an associated record -To associate a file with a record, update the record with the key returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's key, then retrieves the signed URL to download the image. If an image is already associated with the record, this will update the record with the new image. +To associate a file with a record, update the record with the path returned by the Storage upload. The following example uploads the file using Storage, updates the record with the file's path, then retrieves the signed URL to download the image. If an image is already associated with the record, this will update the record with the new image. ```swift title="ContentView" @@ -212,7 +212,7 @@ currentSong.coverArtPath = imagePath let result = try await Amplify.API.mutate(request: .update(currentSong)).get() // Retrieve the file's signed URL: -let signedUrl = try await Amplify.Storage.getURL(key: imagePath) +let signedUrl = try await Amplify.Storage.getURL(path: imagePath) ``` @@ -320,7 +320,7 @@ var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: cu // If the record has no associated file, we can return early. guard song?.coverArtPath != nil else { - print("There is no cover art key to remove image association") + print("There is no cover art path to remove image association") return } @@ -372,7 +372,7 @@ var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: cu // If the record has no associated file, we can return early. guard let coverArtPath = song?.coverArtPath else { - print("There is no cover art key to remove image association") + print("There is no cover art path to remove image association") return } @@ -502,7 +502,7 @@ var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get() let imagePaths = await withTaskGroup(of: String?.self) { group in for imageData in imagesData { group.addTask { - let path = "\(album.id)-\(UUID().uuidString)" + let path = "images/\(album.id)-\(UUID().uuidString)" do { _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData @@ -597,10 +597,9 @@ To associate additional files with a record, update the record with the paths re ```swift title="ContentView" // Upload file to Storage: -let path = "\(currentAlbum.id)-\(UUID().uuidString)" +let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" -_ = try await Amplify.Storage.uploadData(path: .fromString(path), - data: imageData).value +_ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Query existing record to retrieve currently associated files: var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() @@ -689,7 +688,7 @@ Updating a file for an associated record is the same as updating a file for a si ]}> ```swift title="ContentView" // Upload new file to Storage: -let path = "\(currentAlbum.id)-\(UUID().uuidString)" +let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value @@ -744,9 +743,9 @@ if (!photoAlbum?.imagePaths?.length) return; // Retrieve last image path: const [lastImagePath] = photoAlbum.imagePaths.slice(-1); -// Remove last file association by key +// Remove last file association by path const updatedimagePaths = [ - ...photoAlbum.imagePaths.filter((key) => key !== lastImagePath), + ...photoAlbum.imagePaths.filter((path) => path !== lastImagePath), newFilePath, ]; @@ -846,7 +845,7 @@ const signedUrls = await Promise.all( ### Delete and remove files associated with API records -The workflow for deleting and removing files associated with API records is the same as when working with a single file, except that when performing a delete you will need to iterate over the list of files keys and call `Storage.remove()` for each file. +The workflow for deleting and removing files associated with API records is the same as when working with a single file, except that when performing a delete you will need to iterate over the list of file paths and call `Storage.remove()` for each file. #### Remove the file association, continue to persist both files and record @@ -1258,8 +1257,7 @@ class SongViewModel: ObservableObject { var createdSong = try await Amplify.API.mutate(request: .create(song)).get() // Upload the art cover image - _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)"), - data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)"), data: imageData).value // Update the song record with the image path createdSong.coverArtPath = createdSong.id @@ -1287,8 +1285,7 @@ class SongViewModel: ObservableObject { } // Upload the new art image - _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(currentSong.id)"), - data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(currentSong.id)"), data: imageData).value // Update the song record currentSong.coverArtPath = currentSong.id @@ -2673,9 +2670,9 @@ function App({ signOut, user }: WithAuthenticatorProps) { // Retrieve last image path: const [lastImagePath] = photoAlbum.imagePaths.slice(-1); - // Remove last file association by key + // Remove last file association by path const updatedimagePaths = [ - ...photoAlbum.imagePaths.filter((key) => key !== lastImagePath), + ...photoAlbum.imagePaths.filter((path) => path !== lastImagePath), newFilePath, ]; From df76d807ea7946ec2e688602efe84128896d39e7 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 13:16:00 -0400 Subject: [PATCH 10/18] correct instances of .mutate(request: .get) to .query(request: .get) --- .../build-a-backend/data/working-with-files/index.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 939af657ce8..ee360801d4e 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -424,7 +424,7 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() +let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() // Delete the record from the API let deleteResult = try await Amplify.API.mutate(request: .delete(song!)) @@ -1340,7 +1340,7 @@ class SongViewModel: ObservableObject { } // Get the song record - var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() + var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() guard song?.coverArtPath != nil else { print("There is no cover art path to remove image association") @@ -1815,7 +1815,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { print("Error: could not retrieve album") return } From 7c042e516fb6400c3b043badc176927ac1b47d32 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 13:23:13 -0400 Subject: [PATCH 11/18] correct amplify configuration in swift complete example --- .../build-a-backend/data/working-with-files/index.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index ee360801d4e..8ef472ccc4e 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -1077,11 +1077,10 @@ import PhotosUI struct WorkingWithFilesApp: App { init() { - _ = AWSAPIPlugin(modelRegistration: AmplifyModels()) do { Amplify.Logging.logLevel = .verbose try Amplify.add(plugin: AWSCognitoAuthPlugin()) - try Amplify.add(plugin: AWSAPIPlugin()) + try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) try Amplify.add(plugin: AWSS3StoragePlugin()) try Amplify.configure(with: .amplifyOutputs) print("Amplify configured with Auth, API, and Storage plugins") From e494e07e31416875a537eb541d1322b6da465d91 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 13:38:32 -0400 Subject: [PATCH 12/18] add guards to song and album retrieval, remove force unwrapping and optional chaining --- .../data/working-with-files/index.mdx | 103 ++++++++++-------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 8ef472ccc4e..b904b6bdc08 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -64,7 +64,6 @@ export const data = defineData({ }, }, }); - ``` ## Setup the Storage @@ -80,14 +79,11 @@ export const storage = defineStorage({ "images/*": [allow.authenticated.to(["read", "write", "delete"])], }), }); - - ``` Configure the storage in the `amplify/backend.ts` file as demonstrated below: ```ts title="amplify/backend.ts" - import { defineBackend } from "@aws-amplify/backend"; import { auth } from "./auth/resource"; import { data } from "./data/resource"; @@ -98,10 +94,8 @@ export const backend = defineBackend({ data, storage, }); - ``` - ## Configuring authorization Your application needs authorization credentials for reading and writing to both Storage and the Data, except in the case where all data and files are intended to be publicly accessible. @@ -297,7 +291,6 @@ if (!song?.coverArtPath) return; // Retrieve the signed URL: const signedURL = await getUrl({ path: song.coverArtPath }); - ``` @@ -316,7 +309,10 @@ The following example removes the file association from the record, but does not ```swift title="ContentView" // Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return +} // If the record has no associated file, we can return early. guard song?.coverArtPath != nil else { @@ -325,9 +321,9 @@ guard song?.coverArtPath != nil else { } // Remove associated file from record -song?.coverArtPath = nil +song.coverArtPath = nil -let updateResult = try await Amplify.API.mutate(request: .update(song!)).get() +let updateResult = try await Amplify.API.mutate(request: .update(song)).get() ``` @@ -367,19 +363,22 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" -// Get the song record -var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() + // Get the song record + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return + } -// If the record has no associated file, we can return early. -guard let coverArtPath = song?.coverArtPath else { - print("There is no cover art path to remove image association") - return -} + // If the record has no associated file, we can return early. + guard song.coverArtPath != nil else { + print("There is no cover art path to remove image association") + return + } -// Remove associated file from record -song?.coverArtPath = nil + // Remove associated file from record + song.coverArtPath = nil -let updateResult = try await Amplify.API.mutate(request: .update(song!)).get() + let updateResult = try await Amplify.API.mutate(request: .update(song)).get() // Delete the file from S3 try await Amplify.Storage.remove(path: .fromString(coverArtPath)) @@ -602,9 +601,12 @@ let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Query existing record to retrieve currently associated files: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Album may have been deleted, no album by id: ", currentAlbum.id) + return +} -guard var imagePaths = album?.imagePaths else { +guard var imagePaths = album.imagePaths else { print("Album does not contain images") return } @@ -613,9 +615,9 @@ guard var imagePaths = album?.imagePaths else { imagePaths.append(path) // Update record with merged file associations: -album?.imagePaths = imagePaths +album.imagePaths = imagePaths -let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() +let updateResult = try await Amplify.API.mutate(request: .update(album)).get() ``` @@ -854,18 +856,21 @@ The workflow for deleting and removing files associated with API records is the ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Album may have been deleted, no album by id: ", currentAlbum.id) + return +} // If the record has no associated file, we can return early. -guard let imagePaths = album?.imagePaths, !imagePaths.isEmpty else { +guard let imagePaths = album.imagePaths, !imagePaths.isEmpty else { print("There are no images to remove association") return } // Remove the file association from the record: -album?.imagePaths = nil +album.imagePaths = nil -let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() +let updateResult = try await Amplify.API.mutate(request: .update(album)).get() ``` @@ -905,10 +910,13 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ ]}> ```swift title="ContentView" // Get the API record: -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Album may have been deleted, no album by id: ", currentAlbum.id) + return +} // If the record has no associated files, we can return early. -guard let imagePathsOptional = album?.imagePaths else { +guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") return } @@ -916,9 +924,9 @@ guard let imagePathsOptional = album?.imagePaths else { let imagePaths = imagePathsOptional.compactMap { $0 } // Remove associated files from record: -album?.imagePaths = nil +album.imagePaths = nil -let updateResult = try await Amplify.API.mutate(request: .update(album!)).get() +let updateResult = try await Amplify.API.mutate(request: .update(album)).get() // Delete the file from S3 await withTaskGroup(of: Void.self) { group in @@ -979,10 +987,13 @@ await Promise.all( ```swift title="ContentView" // Get the album record -var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Album may have been deleted, no album by id: ", currentAlbum.id) + return +} // If the record has no associated file, we can return early. -guard let imagePathsOptional = album?.imagePaths else { +guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") let deleteResult = try await Amplify.API.mutate(request: .delete(album!)).get() @@ -1005,7 +1016,7 @@ await withTaskGroup(of: Void.self) { group in } // Delete the API record: -let deleteResult = try await Amplify.API.mutate(request: .delete(album!)).get() +let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() ``` @@ -1339,17 +1350,20 @@ class SongViewModel: ObservableObject { } // Get the song record - var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return + } - guard song?.coverArtPath != nil else { + guard song.coverArtPath != nil else { print("There is no cover art path to remove image association") return } // Set the association to nil and update it - song?.coverArtPath = nil + song.coverArtPath = nil - let updatedSong = try await Amplify.API.mutate(request: .update(song!)).get() + let updatedSong = try await Amplify.API.mutate(request: .update(song)).get() await setCurrentSong(updatedSong) } @@ -1368,7 +1382,7 @@ class SongViewModel: ObservableObject { // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Error: could not retrieve the song") + print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -1683,7 +1697,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the latest album guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1729,6 +1743,7 @@ class PhotoAlbumViewModel: ObservableObject { // Update the album with the image paths var album = currentAlbum + if var imagePaths = album.imagePaths { imagePaths.removeLast() imagePaths.append(path) @@ -1815,7 +1830,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1847,7 +1862,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1899,7 +1914,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } From 428abdc117c2d3ee6afbe7e044feee8e7684518d Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 15:17:58 -0400 Subject: [PATCH 13/18] more corrections --- .../data/working-with-files/index.mdx | 195 ++++++++++++------ 1 file changed, 136 insertions(+), 59 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index b904b6bdc08..04962725317 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -123,20 +123,20 @@ The API record's `id` is prepended to the Storage file name to ensure uniqueness let song = Song(name: name) guard let imageData = artCover.pngData() else { - print("Could not get data from UIImage.") + print("Could not get data from image.") return } // Create the song record var createdSong = try await Amplify.API.mutate(request: .create(song)).get() +let coverArtPath = "images/\(createdSong.id)" // Upload the art cover image -_ = Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)-cover.png"), data: imageData) +_ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value // Update the song record with the image path -createdSong.coverArtPath = createdSong.id - -_ = try await Amplify.API.mutate(request: .update(createdSong)).get() +createdSong.coverArtPath = coverArtPath +let updatedSong = try await Amplify.API.mutate(request: .update(createdSong)).get() ``` @@ -195,18 +195,24 @@ To associate a file with a record, update the record with the path returned by t ```swift title="ContentView" -let imagePath = "images/\(currentSong.id)-cover.png" +guard var currentSong = currentSong else { + print("There is no song to associated the image with. Create a Song first.") + return + } -// Upload the new art image -_ = try await Amplify.Storage.uploadData(path: .fromString(imagePath), data: imageData).value +guard let imageData = artCover.pngData() else { + print("Could not get data from UIImage.") + return +} -// Add the file association to the record: -currentSong.coverArtPath = imagePath +let coverArtPath = "images/\(currentSong.id)" -let result = try await Amplify.API.mutate(request: .update(currentSong)).get() +// Upload the new art image +_ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value -// Retrieve the file's signed URL: -let signedUrl = try await Amplify.Storage.getURL(path: imagePath) +// Update the song record +currentSong.coverArtPath = coverArtPath +let updatedSong = try await Amplify.API.mutate(request: .update(currentSong)).get() ``` @@ -258,10 +264,19 @@ To retrieve the file associated with a record, first query the record, then use ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return +} + +// If the record has no associated file, we can return early. +guard coverArtPath = song.coverArtPath else { + print("There is no cover art path to retrieve image") + return +} // Download the art cover -let imageData = try await Amplify.Storage.downloadData(path: .fromString((song?.coverArtPath)!)).value +let imageData = try await Amplify.Storage.downloadData(path: .fromString(song.coverArtPath)).value let image = UIImage(data: imageData) ``` @@ -315,7 +330,7 @@ guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifi } // If the record has no associated file, we can return early. -guard song?.coverArtPath != nil else { +guard song.coverArtPath != nil else { print("There is no cover art path to remove image association") return } @@ -363,22 +378,22 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" - // Get the song record - guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Song may have been deleted, no song by id: ", currentSong.id) - return - } +// Get the song record +guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return +} - // If the record has no associated file, we can return early. - guard song.coverArtPath != nil else { - print("There is no cover art path to remove image association") - return - } +// If the record has no associated file, we can return early. +guard song.coverArtPath != nil else { + print("There is no cover art path to remove image association") + return +} - // Remove associated file from record - song.coverArtPath = nil +// Remove associated file from record +song.coverArtPath = nil - let updateResult = try await Amplify.API.mutate(request: .update(song)).get() +let updateResult = try await Amplify.API.mutate(request: .update(song)).get() // Delete the file from S3 try await Amplify.Storage.remove(path: .fromString(coverArtPath)) @@ -423,13 +438,16 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record -let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() +guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return +} // Delete the record from the API -let deleteResult = try await Amplify.API.mutate(request: .delete(song!)) +let deleteResult = try await Amplify.API.mutate(request: .delete(song)) // Delete the file from S3 -if let coverArt = song?.coverArtPath { +if let coverArt = song.coverArtPath { try await Amplify.Storage.remove(path: .fromString(coverArt)) } ``` @@ -503,8 +521,7 @@ let imagePaths = await withTaskGroup(of: String?.self) { group in group.addTask { let path = "images/\(album.id)-\(UUID().uuidString)" do { - _ = try await Amplify.Storage.uploadData(path: .fromString(path), - data: imageData + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData ).value return path } catch { @@ -523,7 +540,8 @@ let imagePaths = await withTaskGroup(of: String?.self) { group in // Add the file association to the record: createdAlbum.imagePaths = imagePaths -let updateResult = try await Amplify.API.mutate(request: .update(createdAlbum)).get() + +_ = try await Amplify.API.mutate(request: .update(createdAlbum)).get() ``` @@ -987,7 +1005,7 @@ await Promise.all( ```swift title="ContentView" // Get the album record -guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { +guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -996,7 +1014,7 @@ guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byI guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") - let deleteResult = try await Amplify.API.mutate(request: .delete(album!)).get() + let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() return } @@ -1256,7 +1274,7 @@ class SongViewModel: ObservableObject { await setIsLoading(false) } } - let song = Song(name: name) + let song = Song(name: name) guard let imageData = artCover.pngData() else { print("Could not get data from image.") @@ -1265,17 +1283,36 @@ class SongViewModel: ObservableObject { // Create the song record var createdSong = try await Amplify.API.mutate(request: .create(song)).get() + let coverArtPath = "images/\(createdSong.id)" // Upload the art cover image - _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(createdSong.id)"), data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value // Update the song record with the image path - createdSong.coverArtPath = createdSong.id + createdSong.coverArtPath = coverArtPath let updatedSong = try await Amplify.API.mutate(request: .update(createdSong)).get() await setCurrentSong(updatedSong) } + func getSongAndFile(currentSong: Song, imageData: Data) async throws { + // Get the song record + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return + } + + guard let coverArtPath = song.coverArtPath else { + print("There is no cover art path to retrieve image") + return + } + + // Download the art cover + let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value + + let image = UIImage(data: imageData) + } + // Add or update an image for an associated record func updateArtCover(artCover: UIImage) async throws { await setIsLoading(true) @@ -1289,16 +1326,19 @@ class SongViewModel: ObservableObject { print("There is no song to associated the image with. Create a Song first.") return } + guard let imageData = artCover.pngData() else { print("Could not get data from UIImage.") return } + let coverArtPath = "images/\(currentSong.id)" + // Upload the new art image - _ = try await Amplify.Storage.uploadData(path: .fromString("images/\(currentSong.id)"), data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString(coverArtPath), data: imageData).value // Update the song record - currentSong.coverArtPath = currentSong.id + currentSong.coverArtPath = coverArtPath let updatedSong = try await Amplify.API.mutate(request: .update(currentSong)).get() await setCurrentSong(updatedSong) @@ -1319,9 +1359,12 @@ class SongViewModel: ObservableObject { await setCurrentImage(nil) // Get the song record - let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() + guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + print("Song may have been deleted, no song by id: ", currentSong.id) + return + } - guard let coverArtPath = song?.coverArtPath else { + guard let coverArtPath = song.coverArtPath else { print("Song does not contain cover art") await setCurrentSong(song) await setCurrentImage(nil) @@ -1350,7 +1393,7 @@ class SongViewModel: ObservableObject { } // Get the song record - guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + guard var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -1382,7 +1425,7 @@ class SongViewModel: ObservableObject { // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Song may have been deleted, no song by id: ", currentSong.id) + print("Error: could not retrieve the song") return } @@ -1415,8 +1458,8 @@ class SongViewModel: ObservableObject { } // Get the song record - guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Song may have been deleted, no song by id: ", currentSong.id) + guard var song = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)).get() else { + print("Error: could not retrieve the song") return } @@ -1620,7 +1663,8 @@ class PhotoAlbumViewModel: ObservableObject { group.addTask { let path = "images/\(album.id)-\(UUID().uuidString)" do { - _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString(path), + data: imageData).value return path } catch { print("Failed with error:", error) @@ -1672,6 +1716,40 @@ class PhotoAlbumViewModel: ObservableObject { await setCurrentAlbum(updatedAlbum) } + func test(currentAlbum: PhotoAlbum) async throws { + // Get the album record + guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { + print("Album may have been deleted, no album by id: ", currentAlbum.id) + return + } + + // If the record has no associated file, we can return early. + guard let imagePathsOptional = album.imagePaths else { + print("Album does not contain images") + + let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() + return + } + + let imagePaths = imagePathsOptional.compactMap { $0 } + + // Remove the files from S3: + await withTaskGroup(of: Void.self) { group in + for path in imagePaths { + group.addTask { + do { + try await Amplify.Storage.remove(path: .fromString(path)) + } catch { + print("Failed with error:", error) + } + } + } + } + + // Delete the API record: + let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() + } + // Add new file to an associated record func addAdditionalPhotos(_ photo: UIImage) async throws { await setIsLoading(true) @@ -1696,8 +1774,8 @@ class PhotoAlbumViewModel: ObservableObject { _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value // Get the latest album - guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } @@ -1743,7 +1821,6 @@ class PhotoAlbumViewModel: ObservableObject { // Update the album with the image paths var album = currentAlbum - if var imagePaths = album.imagePaths { imagePaths.removeLast() imagePaths.append(path) @@ -1777,7 +1854,7 @@ class PhotoAlbumViewModel: ObservableObject { guard let album = try await Amplify.API.query( request: .get(PhotoAlbum.self, - byIdentifier: currentAlbum.id)).get() else { + byId: currentAlbum.id)).get() else { print("Error: could not retrieve album") return } @@ -1829,8 +1906,8 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) + guard var album = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } @@ -1861,8 +1938,8 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } @@ -1913,8 +1990,8 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } From 15678b1456f5fbcc764f2727fba13166cbaf435d Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 15:34:32 -0400 Subject: [PATCH 14/18] remove test func --- .../data/working-with-files/index.mdx | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 04962725317..337a44530ff 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -1663,8 +1663,7 @@ class PhotoAlbumViewModel: ObservableObject { group.addTask { let path = "images/\(album.id)-\(UUID().uuidString)" do { - _ = try await Amplify.Storage.uploadData(path: .fromString(path), - data: imageData).value + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value return path } catch { print("Failed with error:", error) @@ -1716,40 +1715,6 @@ class PhotoAlbumViewModel: ObservableObject { await setCurrentAlbum(updatedAlbum) } - func test(currentAlbum: PhotoAlbum) async throws { - // Get the album record - guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) - return - } - - // If the record has no associated file, we can return early. - guard let imagePathsOptional = album.imagePaths else { - print("Album does not contain images") - - let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() - return - } - - let imagePaths = imagePathsOptional.compactMap { $0 } - - // Remove the files from S3: - await withTaskGroup(of: Void.self) { group in - for path in imagePaths { - group.addTask { - do { - try await Amplify.Storage.remove(path: .fromString(path)) - } catch { - print("Failed with error:", error) - } - } - } - } - - // Delete the API record: - let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() - } - // Add new file to an associated record func addAdditionalPhotos(_ photo: UIImage) async throws { await setIsLoading(true) @@ -1852,9 +1817,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the song record guard let album = try await Amplify.API.query( - request: - .get(PhotoAlbum.self, - byId: currentAlbum.id)).get() else { + request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { print("Error: could not retrieve album") return } @@ -1865,6 +1828,7 @@ class PhotoAlbumViewModel: ObservableObject { await setCurrentImages([]) return } + let imagePaths = imagePathsOptional.compactMap { $0 } // Download the photos @@ -1990,7 +1954,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { print("Error: could not retrieve album") return } From 37ee983ff6f6795b8f8b3f0d83b6a941b29d5423 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 16:41:46 -0400 Subject: [PATCH 15/18] make top level examples consistent with complete example --- .../data/working-with-files/index.mdx | 146 +++++++++--------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 337a44530ff..4f4405637fd 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -196,10 +196,9 @@ To associate a file with a record, update the record with the path returned by t ```swift title="ContentView" guard var currentSong = currentSong else { - print("There is no song to associated the image with. Create a Song first.") - return - } - + print("There is no song to associated the image with. Create a Song first.") + return +} guard let imageData = artCover.pngData() else { print("Could not get data from UIImage.") return @@ -270,13 +269,14 @@ guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifi } // If the record has no associated file, we can return early. -guard coverArtPath = song.coverArtPath else { - print("There is no cover art path to retrieve image") +guard let coverArtPath = song.coverArtPath else { + print("Song does not contain cover art") return } // Download the art cover -let imageData = try await Amplify.Storage.downloadData(path: .fromString(song.coverArtPath)).value +print("coverArtPath: ", coverArtPath) +let imageData = try await Amplify.Storage.downloadData(path: .fromString(coverArtPath)).value let image = UIImage(data: imageData) ``` @@ -329,16 +329,15 @@ guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifi return } -// If the record has no associated file, we can return early. guard song.coverArtPath != nil else { print("There is no cover art path to remove image association") return } -// Remove associated file from record +// Set the association to nil and update it song.coverArtPath = nil -let updateResult = try await Amplify.API.mutate(request: .update(song)).get() +let updatedSong = try await Amplify.API.mutate(request: .update(song)).get() ``` @@ -368,7 +367,6 @@ const updatedSong = await client.models.Song.update({ id: song.id, coverArtPath: null, }); - ``` @@ -380,22 +378,20 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Song may have been deleted, no song by id: ", currentSong.id) + print("Error: could not retrieve the song") return } -// If the record has no associated file, we can return early. -guard song.coverArtPath != nil else { +guard let coverArtPath = song.coverArtPath else { print("There is no cover art path to remove image association") return } -// Remove associated file from record +// Set the association to nil and update it song.coverArtPath = nil +let updatedSong = try await Amplify.API.mutate(request: .update(song)).get() -let updateResult = try await Amplify.API.mutate(request: .update(song)).get() - -// Delete the file from S3 +// Remove the image try await Amplify.Storage.remove(path: .fromString(coverArtPath)) ``` @@ -438,18 +434,18 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record -guard let song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Song may have been deleted, no song by id: ", currentSong.id) +guard let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else { + print("Error: could not retrieve the song") return } -// Delete the record from the API -let deleteResult = try await Amplify.API.mutate(request: .delete(song)) - -// Delete the file from S3 if let coverArt = song.coverArtPath { + // Delete the file from S3 try await Amplify.Storage.remove(path: .fromString(coverArt)) } + +// Delete the song record +_ = try await Amplify.API.mutate(request: .delete(song)).get() ``` @@ -510,19 +506,17 @@ First create a record via the GraphQL API, then upload the files to Storage, and ```swift title="ContentView" -// Create the API record: +// Create the photo album record let album = PhotoAlbum(name: name) - var createdAlbum = try await Amplify.API.mutate(request: .create(album)).get() -// Upload all files to Storage: +// Upload the photo album images let imagePaths = await withTaskGroup(of: String?.self) { group in for imageData in imagesData { group.addTask { let path = "images/\(album.id)-\(UUID().uuidString)" do { - _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData - ).value + _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value return path } catch { print("Failed with error:", error) @@ -538,10 +532,9 @@ let imagePaths = await withTaskGroup(of: String?.self) { group in return imagePaths.compactMap { $0 } } -// Add the file association to the record: +// Update the album with the image paths createdAlbum.imagePaths = imagePaths - -_ = try await Amplify.API.mutate(request: .update(createdAlbum)).get() +let updatedAlbum = try await Amplify.API.mutate(request: .update(createdAlbum)).get() ``` @@ -613,29 +606,29 @@ To associate additional files with a record, update the record with the paths re ```swift title="ContentView" -// Upload file to Storage: +// Upload the new photo album image let path = "images/\(currentAlbum.id)-\(UUID().uuidString)" - _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageData).value -// Query existing record to retrieve currently associated files: -guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) +// Get the latest album +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } guard var imagePaths = album.imagePaths else { print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) return } -// Add new path to the existing paths +// Add new to the existing paths imagePaths.append(path) -// Update record with merged file associations: +// Update the album with the image paths album.imagePaths = imagePaths - -let updateResult = try await Amplify.API.mutate(request: .update(album)).get() +let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() ``` @@ -796,17 +789,22 @@ To retrieve the files associated with a record, first query the record, then use ```swift title="ContentView" // Query the record to get the file paths: -let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() +guard let album = try await Amplify.API.query( + request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") + return +} -// If the record has no associated files, we can return early. -guard let imagePathsOptional = album?.imagePaths else { +guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) return } let imagePaths = imagePathsOptional.compactMap { $0 } -// Download the files from S3: +// Download the photos let images = await withTaskGroup(of: UIImage?.self) { group in for path in imagePaths { group.addTask { @@ -873,22 +871,20 @@ The workflow for deleting and removing files associated with API records is the "swift" ]}> ```swift title="ContentView" -// Get the API record: -guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) +// Get the album record +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } -// If the record has no associated file, we can return early. guard let imagePaths = album.imagePaths, !imagePaths.isEmpty else { print("There are no images to remove association") return } -// Remove the file association from the record: +// Set the association to nil and update it album.imagePaths = nil - -let updateResult = try await Amplify.API.mutate(request: .update(album)).get() +let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() ``` @@ -927,26 +923,25 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ "swift" ]}> ```swift title="ContentView" -// Get the API record: -guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) +// Get the album record +guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } -// If the record has no associated files, we can return early. guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") + await setCurrentAlbum(album) + await setCurrentImages([]) return } - let imagePaths = imagePathsOptional.compactMap { $0 } -// Remove associated files from record: +// Set the associations to nil and update it album.imagePaths = nil +let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() -let updateResult = try await Amplify.API.mutate(request: .update(album)).get() - -// Delete the file from S3 +// Remove the photos await withTaskGroup(of: Void.self) { group in for path in imagePaths { group.addTask { @@ -957,6 +952,9 @@ await withTaskGroup(of: Void.self) { group in } } } + + for await _ in group { + } } ``` @@ -1005,22 +1003,25 @@ await Promise.all( ```swift title="ContentView" // Get the album record -guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)).get() else { - print("Album may have been deleted, no album by id: ", currentAlbum.id) +guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + print("Error: could not retrieve album") return } -// If the record has no associated file, we can return early. guard let imagePathsOptional = album.imagePaths else { print("Album does not contain images") - let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() + // Delete the album record + _ = try await Amplify.API.mutate(request: .delete(album)) + + await setCurrentAlbum(nil) + await setCurrentImages([]) return } let imagePaths = imagePathsOptional.compactMap { $0 } -// Remove the files from S3: +// Remove the photos await withTaskGroup(of: Void.self) { group in for path in imagePaths { group.addTask { @@ -1031,10 +1032,13 @@ await withTaskGroup(of: Void.self) { group in } } } + + for await _ in group { + } } -// Delete the API record: -let deleteResult = try await Amplify.API.mutate(request: .delete(album)).get() +// Delete the album record +_ = try await Amplify.API.mutate(request: .delete(album)).get() ``` @@ -1393,7 +1397,7 @@ class SongViewModel: ObservableObject { } // Get the song record - guard var song = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { + guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -1458,7 +1462,7 @@ class SongViewModel: ObservableObject { } // Get the song record - guard var song = try await Amplify.API.mutate(request: .get(Song.self, byId: currentSong.id)).get() else { + guard var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else { print("Error: could not retrieve the song") return } @@ -1870,7 +1874,7 @@ class PhotoAlbumViewModel: ObservableObject { } // Get the album record - guard var album = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { + guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { print("Error: could not retrieve album") return } From 27ee9947e1975985716989a47b4b5a4007e02943 Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Fri, 14 Jun 2024 16:47:20 -0400 Subject: [PATCH 16/18] make get errors consistent --- .../data/working-with-files/index.mdx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 4f4405637fd..0f2879b040f 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -378,7 +378,7 @@ The following example removes the file from the record, then deletes the file fr ```swift title="ContentView" // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Error: could not retrieve the song") + print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -435,7 +435,7 @@ await remove({ path: song.coverArtPath }); ```swift title="ContentView" // Get the song record guard let song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else { - print("Error: could not retrieve the song") + print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -612,7 +612,7 @@ _ = try await Amplify.Storage.uploadData(path: .fromString(path), data: imageDat // Get the latest album guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -791,7 +791,7 @@ To retrieve the files associated with a record, first query the record, then use // Query the record to get the file paths: guard let album = try await Amplify.API.query( request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -873,7 +873,7 @@ The workflow for deleting and removing files associated with API records is the ```swift title="ContentView" // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -925,7 +925,7 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ ```swift title="ContentView" // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1004,7 +1004,7 @@ await Promise.all( ```swift title="ContentView" // Get the album record guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1429,7 +1429,7 @@ class SongViewModel: ObservableObject { // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)).get() else { - print("Error: could not retrieve the song") + print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -1463,7 +1463,7 @@ class SongViewModel: ObservableObject { // Get the song record guard var song = try await Amplify.API.query(request: .get(Song.self, byId: currentSong.id)).get() else { - print("Error: could not retrieve the song") + print("Song may have been deleted, no song by id: ", currentSong.id) return } @@ -1744,7 +1744,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the latest album guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1822,7 +1822,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the song record guard let album = try await Amplify.API.query( request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1875,7 +1875,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1907,7 +1907,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard var album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } @@ -1959,7 +1959,7 @@ class PhotoAlbumViewModel: ObservableObject { // Get the album record guard let album = try await Amplify.API.query(request: .get(PhotoAlbum.self, byId: currentAlbum.id)).get() else { - print("Error: could not retrieve album") + print("Album may have been deleted, no album by id: ", currentAlbum.id) return } From 668a356c5392d3ffb7e21300d51d2ae5b22679fa Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Mon, 17 Jun 2024 11:45:26 -0400 Subject: [PATCH 17/18] Update index.mdx Co-authored-by: Heather Buchel --- .../build-a-backend/data/working-with-files/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index 0f2879b040f..fad65e02849 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -919,7 +919,7 @@ const updatedPhotoAlbum = await client.models.PhotoAlbum.update({ #### Remove the record association and delete the files - ```swift title="ContentView" From c0237ceca8acc35dc26a06dc0a7e8a4d4fcfb64d Mon Sep 17 00:00:00 2001 From: Chris Bonifacio Date: Mon, 17 Jun 2024 11:47:52 -0400 Subject: [PATCH 18/18] Correct files -> filters prop --- .../build-a-backend/data/working-with-files/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx index fad65e02849..74fe526a40a 100644 --- a/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx +++ b/src/pages/[platform]/build-a-backend/data/working-with-files/index.mdx @@ -888,7 +888,7 @@ let updatedAlbum = try await Amplify.API.mutate(request: .update(album)).get() ``` - ```ts title="src/App.tsx" @@ -958,7 +958,7 @@ await withTaskGroup(of: Void.self) { group in } ``` - ```ts title="src/App.tsx"