diff --git a/cspell.json b/cspell.json index f33889991db..c43dce26b34 100644 --- a/cspell.json +++ b/cspell.json @@ -1650,7 +1650,12 @@ "posttags", "callouts", "immer", - "ERESOLVE" + "ERESOLVE", + "graphqls", + "swiftpm", + "xcshareddata", + "unsynced", + "vararg" ], "flagWords": ["hte", "full-stack", "Full-stack", "Full-Stack", "sudo"], "patterns": [ diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index a3a92b68720..35465279837 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -1908,12 +1908,41 @@ export const directory = { { path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/add-local-caching/index.mdx' }, - { path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/advanced-patterns/index.mdx' }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/authorization-and-data/index.mdx' + }, { path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/index.mdx' + }, + { + path: 'src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/index.mdx' } ] } diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/authorization-and-data/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/authorization-and-data/index.mdx new file mode 100644 index 00000000000..9b765b25909 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/authorization-and-data/index.mdx @@ -0,0 +1,52 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Authorization and data handling', + description: 'Manage authorization rules and handle existing customer data during the migration from DataStore to Amplify API for Flutter.', + platforms: [ + 'flutter' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +## Authorization rules + +As part of the transition from Amplify DataStore, it is essential to understand how to manage authorization rules for your models using AWS AppSync, which continues to serve as the source of truth for all auth rules. While DataStore previously handled the synchronization and application of these rules automatically, the responsibility now shifts to you as the developer to integrate these auth rules into your chosen local data storage solution. + +Here are some key considerations: + +- **AppSync as the source of truth:** AWS AppSync remains the authoritative service for defining and enforcing authorization rules for your models. Ensure that your local data handling and synchronization strategies are aligned with the rules set in AppSync to maintain security and data consistency. +- **Managing local storage:** You are responsible for implementing the logic to save data to your selected local storage solution. This includes ensuring that your offline models are compatible with the authorization rules defined in AppSync. +- **Clearing local storage on sign-out:** As a best practice, clear your local storage whenever a user signs out of your application. This helps to prevent unauthorized access to data and ensures that each user's session begins with a fresh, secure state aligned with the current auth rules. + +## Handle existing customer data + +As you transition from Amplify DataStore to a new local storage solution, it is important to carefully manage the data currently stored on users' devices. The approach varies depending on whether your application is connected to AWS AppSync for remote synchronization or operates entirely in a local-only mode. + +### Connected apps using AWS AppSync + +If your application connects to AWS AppSync for data synchronization, AWS AppSync should be treated as the single source of truth. During migration: + +1. **Sync unsynced data.** Before transitioning to the new local store, ensure that any unsynced data on the device is successfully pushed to AWS AppSync. +2. **Re-sync from AppSync.** Once the unsynced data is uploaded, clear the existing local storage and initialize the new local store. Use AWS AppSync to re-sync all necessary data down to the device. +3. **Validate data.** After re-syncing, perform validation checks to confirm that the data in your new local store matches the data in AWS AppSync. + +### Local-only users + +If your application operates without a remote sync to AWS AppSync, handle the migration of local data manually. + +1. **Back up local data.** Before starting the migration, create a backup of the existing local data. +2. **Query and migrate local data.** Query all existing data from the current local store. Depending on the structure of your data, you may need to transform or reformat the data to fit the schema of your new local storage solution. +3. **Map data.** Carefully map the data from your existing store to your new solution, ensuring that all fields and relationships are preserved. diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/index.mdx index d2d733e78a2..334da00e835 100644 --- a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/index.mdx +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/index.mdx @@ -4,11 +4,14 @@ export const meta = { title: 'Disable conflict resolution', description: 'Disable AppSync conflict resolution and prepare your backend for migration from DataStore to a standard GraphQL client with zero downtime.', platforms: [ + 'android', 'angular', + 'flutter', 'javascript', 'nextjs', 'react', 'react-native', + 'swift', 'vue' ] }; @@ -28,7 +31,7 @@ export function getStaticProps(context) { -**Complete the frontend migration first.** This page covers the **final backend step** of the migration. Before following these instructions, migrate all frontend clients from DataStore to Apollo Client (or the Amplify API category) using the preceding pages in this guide. Disabling conflict resolution while any client still uses DataStore will break that client immediately. +**Complete the frontend migration first.** This page covers the **final backend step** of the migration. Before following these instructions, migrate all frontend clients from DataStore using the preceding pages in this guide. Disabling conflict resolution while any client still uses DataStore will break that client immediately. diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/index.mdx new file mode 100644 index 00000000000..57afd517603 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/index.mdx @@ -0,0 +1,131 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Helpful resources', + description: 'Additional documentation, third-party packages, and references for the DataStore migration.', + platforms: [ + 'android', + 'flutter', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + + + +## Amplify API + +- [Amplify API Category — GraphQL](https://docs.amplify.aws/gen1/flutter/build-a-backend/graphqlapi/) +- [Amplify API — Mutate Data](https://docs.amplify.aws/gen1/flutter/build-a-backend/graphqlapi/mutate-data/) +- [Amplify API — Query Data](https://docs.amplify.aws/gen1/flutter/build-a-backend/graphqlapi/query-data/) +- [Amplify API — Subscribe to Data](https://docs.amplify.aws/gen1/flutter/build-a-backend/graphqlapi/subscribe-data/) + +## AWS AppSync + +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) + +## Amplify DataStore (Legacy) + +- [How DataStore works](https://docs.amplify.aws/gen1/flutter/build-a-backend/more-features/datastore/how-it-works/) +- [Schema updates](https://docs.amplify.aws/gen1/flutter/build-a-backend/more-features/datastore/schema-updates/) + +## General + +- [Android Offline First Guide](https://developer.android.com/topic/architecture/data-layer/offline-first) +- [Exponential Backoff and Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) + +## Local storage packages + +While migrating off of DataStore, we found a few third-party packages to share. We do not maintain any of these packages and our experiences are limited to the versions we tested on. + +| Feature | [isar-community](https://pub.dev/packages/isar_community) | [Drift](https://pub.dev/packages/drift) | [sqlite3](https://pub.dev/packages/sqlite3) | +|---|---|---|---| +| Model Schemas | ✅ | ✅ | ✅ | +| Nested Models | ✅ | ✅ | ✅ | +| Custom Primary Key | ✅ | ✅ | ✅ | +| CRUD | ✅ | ✅ | ✅ | +| Query Predicates | ✅ | [Have to use where()](https://drift.simonbinder.eu/docs/dart-api/select/#where) | ✅ | +| Observe/ObserveQuery | ✅ | ✅ | [Via `updates` stream](https://pub.dev/documentation/sqlite3/latest/common/CommonDatabase/updates.html) | + + + +Isar does not support Gradle 8, which is a requirement for Amplify Flutter Android, but there is a [fork](https://github.com/isar/isar/issues/1470#issuecomment-1978766200) that resolves this. Drift is used with the Amplify Flutter packages, which can result in restrictions for what Drift versions you can use. + + + + + + + +## AWS AppSync + +- [AWS AppSync Apollo Extensions documentation (Swift)](https://docs.amplify.aws/swift/build-a-backend/data/aws-appsync-apollo-extensions/) +- [AWS AppSync Apollo Extensions GitHub repository](https://github.com/aws-amplify/aws-appsync-apollo-extensions-swift) +- [AWS AppSync Delta Sync guide](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html) +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) + +## Apollo iOS + +- [Apollo iOS documentation](https://www.apollographql.com/docs/ios) +- [Apollo iOS Getting Started](https://www.apollographql.com/docs/ios/get-started) +- [Apollo iOS GitHub repository](https://github.com/apollographql/apollo-ios) +- [Apollo iOS Normalized Cache](https://www.apollographql.com/docs/ios/caching/introduction) +- [Apollo iOS Watching Queries](https://www.apollographql.com/docs/ios/fetching/queries#watching-queries) + +## Amplify DataStore (Legacy) + +- [How DataStore works](https://docs.amplify.aws/gen1/swift/build-a-backend/more-features/datastore/how-it-works/) +- [Schema updates](https://docs.amplify.aws/gen1/swift/build-a-backend/more-features/datastore/schema-updates/) +- [Client code generation (Amplify CLI)](https://docs.amplify.aws/gen1/swift/tools/cli-legacy/client-codegen/) + +## Local storage frameworks + +- [SwiftData documentation](https://developer.apple.com/documentation/swiftdata/) +- [CoreData documentation](https://developer.apple.com/documentation/coredata) + + + + + +## Android + +- [Android Offline First Guide](https://developer.android.com/topic/architecture/data-layer/offline-first) + +## AWS AppSync + +- [AWS AppSync Apollo Extensions documentation](https://docs.amplify.aws/android/build-a-backend/data/aws-appsync-apollo-extensions/) +- [AWS AppSync Delta Sync guide](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html) +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) + +## Apollo Kotlin + +- [Apollo Kotlin documentation](https://www.apollographql.com/docs/kotlin) +- [Apollo Kotlin Getting Started](https://www.apollographql.com/docs/kotlin#getting-started) +- [Apollo Kotlin Gradle Plugin configuration](https://www.apollographql.com/docs/kotlin/advanced/plugin-configuration) +- [Apollo Kotlin Normalized Cache](https://www.apollographql.com/docs/kotlin/caching/normalized-cache) +- [Apollo Kotlin Query Watchers](https://www.apollographql.com/docs/kotlin/caching/query-watchers) + +## Amplify DataStore (Legacy) + +- [How DataStore works](https://docs.amplify.aws/gen1/android/build-a-backend/more-features/datastore/how-it-works/) +- [Schema updates](https://docs.amplify.aws/gen1/android/build-a-backend/more-features/datastore/schema-updates/) +- [Client code generation (Amplify CLI)](https://docs.amplify.aws/gen1/android/tools/cli-legacy/client-codegen/) + +## Local storage libraries + +- [Room (Android)](https://developer.android.com/training/data-storage/room) +- [SQLDelight](https://sqldelight.github.io/sqldelight) + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/index.mdx index 4fb9d689e78..e0865b6afef 100644 --- a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/index.mdx +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/index.mdx @@ -3,14 +3,17 @@ import { getChildPageNodes } from '@/utils/getChildPageNodes'; export const meta = { title: 'Migrate from DataStore', - description: 'Learn how to migrate from Amplify DataStore to Apollo Client for queries, mutations, and caching with Amplify subscriptions for real-time updates.', + description: 'Learn how to migrate from Amplify DataStore to Apollo Client or the Amplify API category.', route: '/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore', platforms: [ + 'android', 'angular', + 'flutter', 'javascript', 'nextjs', 'react', 'react-native', + 'swift', 'vue' ] }; @@ -32,16 +35,40 @@ export function getStaticProps(context) { ## Understanding DataStore -AWS Amplify DataStore provided a local-first data layer that automatically synchronized data between your app and the cloud. When you used DataStore, you got several powerful capabilities without writing any synchronization logic yourself: a local database (IndexedDB in the browser) that persisted data across sessions, automatic bidirectional sync with your AppSync backend, built-in conflict resolution using version tracking, full offline support with mutation queuing and replay, and real-time updates through `observe()` and `observeQuery()`. +AWS Amplify DataStore provided a local-first data layer that automatically synchronized data between your app and the cloud. When you used DataStore, you got several powerful capabilities without writing any synchronization logic yourself: a local database that persisted data across sessions, automatic bidirectional sync with your AppSync backend, built-in conflict resolution using version tracking, full offline support with mutation queuing and replay, and real-time updates through `observe()` and `observeQuery()`. DataStore abstracted away the complexity of GraphQL operations, network state management, and data consistency. You worked with simple `save`, `query`, and `delete` methods on local models, and DataStore handled everything else behind the scenes. + + This guide shows you how to use Apollo Client for queries, mutations, and caching, combined with the Amplify library's built-in subscription support for real-time updates. Depending on how much of DataStore's feature set your app actually uses, you may find the migration simpler than expected. + + + + +This guide shows you how to migrate from DataStore to the Amplify API category for queries, mutations, and subscriptions. Depending on how much of DataStore's feature set your app actually uses, you may find the migration simpler than expected. + + + + + +This guide shows you how to migrate from DataStore to [Apollo Kotlin](https://www.apollographql.com/docs/kotlin) and the [AWS AppSync Apollo Extensions library](https://docs.amplify.aws/android/build-a-backend/data/aws-appsync-apollo-extensions/) for queries, mutations, and subscriptions. Depending on how much of DataStore's feature set your app actually uses, you may find the migration simpler than expected. + + + + + +This guide shows you how to migrate from DataStore to [Apollo iOS](https://www.apollographql.com/docs/ios) and the [AWS AppSync Apollo Extensions library](https://docs.amplify.aws/swift/build-a-backend/data/aws-appsync-apollo-extensions/) for queries, mutations, and subscriptions. Depending on how much of DataStore's feature set your app actually uses, you may find the migration simpler than expected. + + + Once frontend clients have been migrated off DataStore, [disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/) on the backend to remove the sync infrastructure, simplify mutations (no more `_version` tracking), and switch from soft deletes to hard deletes. ## What this guide covers + + This guide walks you through the migration path from DataStore to Apollo Client. You will set up Apollo Client with your AppSync endpoint, migrate all CRUD operations and relationships, and optionally add persistent caching and optimistic updates. ## Quick comparison: before and after @@ -178,4 +205,194 @@ If you followed the optional [Add local caching](/gen1/[platform]/build-a-backen + + + + +This guide walks you through the migration path from DataStore to the Amplify API category. You will replace DataStore operations with API equivalents, and optionally add local caching and offline-first support using third-party Flutter packages. + +## Quick comparison: before and after + +Here is a quick look at how common DataStore operations translate to the Amplify API category: + +| DataStore Operation | Amplify API Equivalent | +|---------------------|------------------------| +| `Amplify.DataStore.save()` (create) | `Amplify.API.mutate(request: ModelMutations.create(...))` | +| `Amplify.DataStore.save()` (update) | `Amplify.API.mutate(request: ModelMutations.update(...))` | +| `Amplify.DataStore.delete()` | `Amplify.API.mutate(request: ModelMutations.delete(...))` | +| `Amplify.DataStore.query()` (single) | `Amplify.API.query(request: ModelQueries.get(...))` | +| `Amplify.DataStore.query()` (list) | `Amplify.API.query(request: ModelQueries.list(...))` | +| `Amplify.DataStore.observe()` | `Amplify.API.subscribe(ModelSubscriptions.onCreate/onUpdate/onDelete(...))` | +| `Amplify.DataStore.observeQuery()` | Initial `ModelQueries.list()` + three subscriptions (no direct equivalent) | +| `Amplify.DataStore.clear()` | No longer needed (no local DataStore to clear) | +| `Amplify.DataStore.start()` / `stop()` | No longer needed | + +## Use cases + +Not all customers need the full extent of DataStore's capabilities. Depending on your specific use case, migration can be straightforward: + +1. **Ease of API:** If your main reason for using DataStore was to simplify managing GraphQL queries or APIs, and your application does not need offline or local caching, migrate to the Amplify API category. This allows you to continue working with a simplified data management interface while consuming your existing Amplify resources. +2. **Local caching:** If you used DataStore primarily as a local caching layer, storing data temporarily on the device but not relying heavily on its offline-first capabilities, consider migrating to a simpler local caching solution using third-party Flutter packages. +3. **Offline-first:** If your business requirements rely on fully supporting offline-first functionality, the [offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/) guide provides a high-level approach for migrating to a new offline-first solution. + +## How to use this guide + +Follow the pages in order. Each step builds on the previous one. + +1. **[Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/).** Remove the DataStore dependency from your project and configure the API plugin to use your models. + +2. **[Migrate to Amplify API](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/).** Replace all DataStore operations with API equivalents. Migrate `save`, `query`, `delete`, `observe`, and `observeQuery`. + +3. **[Optional: Local caching](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/).** Set up a local cache for offline data access using third-party Flutter packages. + +4. **[Offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/).** Build full offline support with remote sync, live syncing, network detection, and offline mutations. + +5. **[Authorization and data handling](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/authorization-and-data/).** Manage auth rules and handle existing customer data during migration. + +6. **[Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/).** Once ALL frontend clients are migrated off DataStore, remove the sync infrastructure, simplify mutations, and switch from soft deletes to hard deletes. + +7. **[Helpful resources](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/).** Additional documentation, third-party packages, and references. + +## Migration checklist + +Use this checklist to plan and track your migration: + +1. **Update `pubspec.yaml`** — Remove the `amplify_datastore` dependency (see [Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/)) +2. **Update Amplify configuration** — Remove the DataStore plugin and pass `ModelProvider` to the API plugin +3. **Migrate CRUD operations** — Replace `DataStore.save()`, `DataStore.query()`, and `DataStore.delete()` with API equivalents (see [Migrate to Amplify API](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/)) +4. **Migrate real-time listeners** — Replace `DataStore.observe()` and `DataStore.observeQuery()` with API subscriptions +5. **Handle API response differences** — Adapt to `GraphQLResponse` types and token-based pagination +6. **Test** — Verify all CRUD operations, real-time subscriptions, and error handling +7. **Disable conflict resolution** — Once all clients are migrated (see [Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/)) + + + + + +This guide walks you through the migration path from DataStore to Apollo Kotlin. You will set up Apollo Kotlin with your AppSync endpoint, migrate all CRUD operations, and optionally add local caching and offline-first support. + +## Quick comparison: before and after + +Here is a quick look at how common DataStore operations translate to Apollo Kotlin: + +| DataStore Operation | Apollo Kotlin Equivalent | +|---------------------|--------------------------| +| `Amplify.DataStore.save()` | Apollo mutations (`CreatePostMutation`, `UpdatePostMutation`) | +| `Amplify.DataStore.delete()` | Apollo mutations (`DeletePostMutation`) | +| `Amplify.DataStore.query()` | Apollo queries (`GetPostQuery`, `GetPostsQuery`) | +| `Amplify.DataStore.observe()` | Apollo subscriptions (`OnCreateSubscription`, etc.) via `toFlow()` | +| `Amplify.DataStore.observeQuery()` | Apollo normalized cache + `watch()` | +| `Amplify.DataStore.clear()` | No longer needed (no local DataStore to clear) | +| `Amplify.DataStore.start()` / `stop()` | No longer needed | + +## Use cases + +Not all customers need the full extent of DataStore's capabilities. Depending on your specific use case, migration can be straightforward: + +1. **Local caching:** If you used DataStore primarily as a local caching layer, storing data temporarily on the device but not relying heavily on its offline-first capabilities, Apollo Kotlin can provide lightweight data caching without the complexities of managing full offline sync, making it a good fit if your application is mostly connected and only occasionally requires offline access. +2. **Offline-first:** If your business requirements rely on fully supporting offline-first functionality, the [offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/) guide provides a high-level approach for migrating to a new offline-first solution. + +## How to use this guide + +Follow the pages in order. Each step builds on the previous one. + +1. **[Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/).** Remove the DataStore dependency and plugin from your project. + +2. **[Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/).** Retrieve your AppSync schema and define your GraphQL operations for Apollo code generation. + +3. **[Set up Apollo Kotlin](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/).** Add dependencies, configure the Gradle plugin, and set up the Apollo client with your AppSync endpoint and authentication. + +4. **[Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/).** Replace all DataStore operations with Apollo equivalents. Migrate `save`, `query`, `delete`, `observe`, and `observeQuery`. + +5. **[Optional: Local caching](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/).** Set up a local cache using Apollo's normalized cache or a custom caching layer with Room. + +6. **[Offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/).** Build full offline support with remote sync, live syncing, network detection, and offline mutations. + +7. **[Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/).** Once ALL frontend clients are migrated off DataStore, remove the sync infrastructure, simplify mutations, and switch from soft deletes to hard deletes. + +8. **[Helpful resources](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/).** Additional documentation, third-party packages, and references. + +## Migration checklist + +Use this checklist to plan and track your migration: + +1. **Remove the DataStore dependency** — Remove `com.amplifyframework:aws-datastore` from your `build.gradle.kts` (see [Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/)) +2. **Remove the DataStore plugin** — Remove `AWSDataStorePlugin()` from your `Amplify.addPlugin()` calls +3. **Delete generated model files** — Remove DataStore-generated model classes +4. **Set up schema and operations** — Retrieve your AppSync schema and define GraphQL operations (see [Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/)) +5. **Set up Apollo Kotlin** — Add dependencies and configure the Apollo client (see [Set up Apollo Kotlin](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/)) +6. **Migrate CRUD operations** — Replace `DataStore.save()`, `DataStore.query()`, and `DataStore.delete()` with Apollo equivalents (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) +7. **Migrate real-time listeners** — Replace `DataStore.observe()` and `DataStore.observeQuery()` with Apollo subscriptions and cache watchers +8. **Test** — Verify all CRUD operations, real-time subscriptions, and error handling +9. **Disable conflict resolution** — Once all clients are migrated (see [Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/)) + + + + + +This guide walks you through the migration path from DataStore to Apollo iOS. You will set up Apollo iOS with your AppSync endpoint, migrate all CRUD operations, and optionally add local caching and offline-first support. + +## Quick comparison: before and after + +Here is a quick look at how common DataStore operations translate to Apollo iOS: + +| DataStore Operation | Apollo iOS Equivalent | +|---------------------|--------------------------| +| `Amplify.DataStore.save()` | Apollo mutations (`CreatePostMutation`, `UpdatePostMutation`) | +| `Amplify.DataStore.delete()` | Apollo mutations (`DeletePostMutation`) | +| `Amplify.DataStore.query()` | Apollo queries (`GetPostQuery`, `GetPostsQuery`) | +| `Amplify.DataStore.observe()` | Apollo subscriptions (`OnCreateSubscriptionSubscription`, etc.) | +| `Amplify.DataStore.observeQuery()` | Apollo normalized cache + `watch()` | +| `Amplify.DataStore.clear()` | `apolloClient.store.clearCache()` | +| `Amplify.DataStore.start()` / `stop()` | No longer needed | + +## Use cases + +Not all customers need the full extent of DataStore's capabilities. Depending on your specific use case, migration can be straightforward: + +1. **Local caching:** If you used DataStore primarily as a local caching layer, storing data temporarily on the device but not relying heavily on its offline-first capabilities, Apollo iOS can provide lightweight data caching without the complexities of managing full offline sync, making it a good fit if your application is mostly connected and only occasionally requires offline access. +2. **Offline-first:** If your business requirements rely on fully supporting offline-first functionality, the [offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/) guide provides a high-level approach for migrating to a new offline-first solution. + + + +**Version compatibility:** The [AWS AppSync Apollo Extensions library](https://github.com/aws-amplify/aws-appsync-apollo-extensions-swift) currently requires **Apollo iOS 1.x** (not 2.x). All versions of `aws-appsync-apollo-extensions-swift` (1.0.0–1.0.5) depend on Apollo iOS `1.0.0..<2.0.0`. Make sure you use Apollo iOS 1.x throughout this guide. If you install Apollo iOS 2.x alongside the extensions library, Swift Package Manager will fail to resolve dependencies. + + + +## How to use this guide + +Follow the pages in order. Each step builds on the previous one. + +1. **[Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/).** Remove the DataStore frameworks and plugin from your project. + +2. **[Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/).** Retrieve your AppSync schema and define your GraphQL operations for Apollo code generation. + +3. **[Set up Apollo iOS](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/).** Add dependencies, install the CLI, run code generation, and configure the Apollo client with your AppSync endpoint and authentication. + +4. **[Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/).** Replace all DataStore operations with Apollo equivalents. Migrate `save`, `query`, `delete`, `observe`, and `observeQuery`. + +5. **[Optional: Local caching](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/).** Set up a local cache using Apollo's normalized cache or a custom caching layer with SwiftData. + +6. **[Offline-first](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/).** Build full offline support with remote sync, live syncing, network detection, and offline mutations. + +7. **[Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/).** Once ALL frontend clients are migrated off DataStore, remove the sync infrastructure, simplify mutations, and switch from soft deletes to hard deletes. + +8. **[Helpful resources](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/).** Additional documentation and references. + +## Migration checklist + +Use this checklist to plan and track your migration: + +1. **Remove DataStore frameworks** — Remove `AWSDataStorePlugin` and `AWSAPIPlugin` from your target (see [Remove DataStore](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/)) +2. **Remove the DataStore plugin** — Remove `AWSDataStorePlugin` from your `Amplify.add(plugin:)` calls +3. **Delete generated model files** — Remove DataStore-generated model classes (`Post.swift`, `Post+Schema.swift`, `AmplifyModels.swift`) +4. **Set up schema and operations** — Retrieve your AppSync schema and define GraphQL operations (see [Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/)) +5. **Set up Apollo iOS** — Add dependencies, install CLI, run code generation, and configure the Apollo client (see [Set up Apollo iOS](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/)) +6. **Migrate CRUD operations** — Replace `Amplify.DataStore.save()`, `Amplify.DataStore.query()`, and `Amplify.DataStore.delete()` with Apollo equivalents (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) +7. **Migrate real-time listeners** — Replace `Amplify.DataStore.observe()` and `Amplify.DataStore.observeQuery()` with Apollo subscriptions and cache watchers +8. **Test** — Verify all CRUD operations, real-time subscriptions, and error handling +9. **Disable conflict resolution** — Once all clients are migrated (see [Disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/)) + + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/index.mdx new file mode 100644 index 00000000000..f21987b7a90 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/local-caching/index.mdx @@ -0,0 +1,310 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Optional: Local caching', + description: 'Set up local caching as an alternative to DataStore local storage for Flutter and Android.', + platforms: [ + 'android', + 'flutter', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +To guarantee quick and continuous access for customers, Amplify DataStore employs a local cached version of remote data. During your transition from Amplify DataStore, you should choose a local storage solution that aligns with your models to avoid unnecessary complexity. + + + +See the [helpful resources](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/) page for third-party package suggestions. + +## Local models + +You will need to create a separate model for local storage that corresponds to your Amplify GraphQL generated model. To exclusively use your Amplify models, you must develop functions for mapping between your Amplify and local models, ensuring the local model remains concealed from the rest of your implementation. + +```dart +static LocalModel _toLocalModel(RemoteModel remote) { + return LocalModel( + id: remote.id, + someString: remote.someString, + createdAt: remote.createdAt?.getDateTimeInUtc(), + updatedAt: remote.updatedAt?.getDateTimeInUtc(), + ); +} + +static RemoteModel _toRemoteModel(LocalModel local) { + return RemoteModel( + id: local.id, + someString: local.someString, + ); +} +``` + +Certain database libraries lack or offer limited support for custom primary keys. You have the option to use the libraries' limited primary keys alongside your model's primary keys, but it is advisable to choose a database that accommodates the structure of your models. When changing your schema version, DataStore will discard your data to prevent any model incompatibilities. + +## CRUD + +The standard practice in local store operations involves the implementation of interfaces for creating, reading, updating, and deleting. However, it is crucial to consider the relationships between your models. In SQL, when handling 1:1 or 1:many relationships with models, it becomes necessary to create multiple tables and corresponding joins for CRUD operations. When dealing with NoSQL, a basic query for models with a many-to-many relationship may require the use of multiple sub-queries to link the relational data. + +### Queries + +Amplify DataStore uses query predicates, pagination, and sort by parameters to generate queries for Amplify API and its local store solution. Storing all data locally simplifies implementation by only requiring handling of local store queries. Otherwise, you have to generate matching queries for Amplify API and your local store. + +### Observability + +To support a responsive app, pick a local store that supports observing data changes. Alternatively, you can create your own observe Stream that you trigger after every create, update, or delete. + +```dart +Stream observe() { + return localRepository.observe(); +} +``` + + + + + +DataStore directly uses [SQLite](https://github.com/stephencelis/SQLite.swift) APIs, but the option you choose may depend on your caching requirements. + +## Apollo normalized cache + +Apollo iOS includes the option to use SQLite as a [normalized cache](https://www.apollographql.com/docs/ios/caching/introduction). The normalized cache is straightforward to set up and allows Apollo to replay previous queries from the cache to improve latency, reduce bandwidth consumption, and replay queries while offline. + +For setup instructions on the normalized cache, see the [Migrate DataStore to Apollo — ObserveQuery](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/#observequery) section which covers the `watch()` function that leverages the cache. + +## Custom caching with SwiftData + +For more advanced use cases where you want richer access to cached data, you can use your own caching layer instead of, or in addition to, Apollo's provided cache. Two popular frameworks for this approach are [SwiftData](https://developer.apple.com/documentation/swiftdata) and [CoreData](https://developer.apple.com/documentation/coredata). The following examples use SwiftData. + +### Local models + +You can use the `Model()` macro to annotate a separate entity for local storage that corresponds to your Apollo generated model: + +```swift +@Model +class PostEntity { + @Attribute(.unique) var id: String + var updatedAt: String + var createdAt: String + var status: PostEntityStatus? + var title: String + var rating: Int? + var content: String? + + init(id: String, + updatedAt: String, + createdAt: String, + status: PostEntityStatus? = nil, + title: String, + rating: Int? = nil, + content: String? = nil) { + self.id = id + self.updatedAt = updatedAt + self.createdAt = createdAt + self.status = status + self.title = title + self.rating = rating + self.content = content + } +} + +enum PostEntityStatus: Codable { + case active + case inactive +} +``` + +You can then configure the model storage using `ModelContainer`: + +```swift +let container = try ModelContainer(for: PostEntity.self) +``` + +### Model mapping + +Extension initializers can help map the remote model to your local model: + +```swift +extension PostEntity { + convenience init(postDetails: PostDetails) { + self.init(id: postDetails.id, + updatedAt: postDetails.updatedAt ?? "", + createdAt: postDetails.createdAt ?? "", + status: (postDetails.status == .case(.active)) ? .active : .inactive, + title: postDetails.title, + rating: postDetails.rating, + content: postDetails.content) + } +} +``` + +### Populate the cache + +Once your SwiftData models have been set up, you can populate the data by writing the results of Apollo queries into the cache: + +```swift +func populate(apolloClient: ApolloClient, context: ModelContext) { + var nextToken: String? = nil + repeat { + let query = GetPostsQuery( + nextToken: nextToken.flatMap { .some($0) } ?? .none) + apolloClient.fetch(query: query) { result in + guard let data = try? result.get().data else { return } + if let items = data.listPosts?.items { + for item in items { + guard let postDetails = item?.fragments.postDetails + else { continue } + let model = PostEntity(postDetails: postDetails) + context.insert(model) + try? context.save() + } + } + nextToken = data.listPosts?.nextToken + } + } while (nextToken != nil) +} +``` + +### Observability + +You can observe the [`.NSPersistentStoreRemoteChange`](https://developer.apple.com/documentation/foundation/nsnotification/name/3180044-nspersistentstoreremotechange) notification to listen to changes in the persistent store. The notification object contains the transaction's history token which you can use to fetch and process the history of transactions. + + + + + +DataStore directly uses Android's built-in SQLite database APIs, but the option you choose may depend on your caching requirements. See the [helpful resources](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/helpful-resources/) page for library suggestions. + +## Apollo normalized cache + +Apollo Kotlin includes the option to use SQLite as a [normalized cache](https://www.apollographql.com/docs/kotlin/caching/normalized-cache#sqlite-cache). The normalized cache is straightforward to set up and allows Apollo to replay previous queries from the cache to improve latency, reduce bandwidth consumption, and replay queries while offline. + +For setup instructions on the normalized cache, see the [Migrate DataStore to Apollo — ObserveQuery](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/#observequery) section which covers adding the dependency and configuring the cache. + +## Custom caching with Room + +For more advanced use cases where you want richer access to cached data, you can use your own caching layer instead of, or in addition to, Apollo's provided cache. Two popular libraries for this approach are [SQLDelight](https://sqldelight.github.io/sqldelight) and [Room](https://developer.android.com/training/data-storage/room). The following examples use Room. + +### Local models + +To cache models in your own cache, create a separate entity for local storage that corresponds to your Apollo generated model. In Room, the entity corresponding to the `PostDetails` fragment might look like this: + +```kotlin +@Entity(tableName = "post") +data class PostEntity( + @PrimaryKey val id: String, + val updatedAt: String, + val createdAt: String, + val title: String, + val content: String, + val status: PostStatus, + val rating: Int +) +``` + +You can then store, query, and mutate these entities with a DAO: + +```kotlin +@Dao +interface PostDao { + @Query("SELECT * FROM post") + suspend fun getAll(): List + + @Query("SELECT * FROM post WHERE id = :id") + suspend fun get(id: String): PostEntity + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(vararg posts: PostEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(posts: List) + + @Delete + suspend fun delete(post: PostEntity) +} +``` + +### Model mapping + +Extension functions let you map one model type to the other: + +```kotlin +fun PostDetails.toLocalModel() = PostEntity( + id = id, + updatedAt = updatedAt, + createdAt = createdAt, + title = title, + content = content, + status = status, + rating = rating +) + +fun PostEntity.toRemoteModel() = PostDetails( + id = id, + updatedAt = updatedAt, + createdAt = createdAt, + title = title, + content = content, + status = status, + rating = rating +) +``` + +When changing your schema version, DataStore will discard your data to prevent any model incompatibilities. In Room, you can instead use a [migration](https://developer.android.com/training/data-storage/room/migrating-db-versions) to keep your cached data intact. + +### Populate the cache + +Once your Room database has been created you can populate the data by writing the results of Apollo queries into the cache. The following code saves all `Post` objects in the local database: + +```kotlin +suspend fun syncAllPosts() { + // Sync all pages of posts until there is no nextToken + var nextToken: String? = null + do { + val query = GetPostsQuery(nextToken = Optional.presentIfNotNull(nextToken)) + val response = apolloClient.query(query).execute() + response.data?.listPosts?.items?.let { posts -> + val mapped = posts.mapNotNull { it?.postDetails?.toLocalModel() } + postDao.insertAll(mapped) + } + nextToken = response.data?.listPosts?.nextToken + } while (nextToken != null) +} +``` + +### Observability + +To support a responsive app, pick a local store that supports observing data changes. When using Room, you can react to changes in the database by changing your DAO to [return a Kotlin Flow](https://developer.android.com/training/data-storage/room/async-queries#observable). The updated observable DAO looks like this: + +```kotlin +@Dao +interface PostDao { + @Query("SELECT * FROM post") + fun getAll(): Flow> + + @Query("SELECT * FROM post WHERE id = :id") + fun get(id: String): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(vararg posts: PostEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(posts: List) + + @Delete + suspend fun delete(post: PostEntity) +} +``` + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/index.mdx new file mode 100644 index 00000000000..ebc9589f032 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/index.mdx @@ -0,0 +1,315 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Migrate DataStore to Apollo', + description: 'Replace every Amplify DataStore operation with its Apollo Kotlin or Apollo iOS equivalent.', + platforms: [ + 'android', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + + + +This page covers how to replace every `Amplify.DataStore.*` call with the equivalent Apollo Kotlin operation. The examples below use the schema and GraphQL operations created in the [Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/) page. + + + + + +This page covers how to replace every `Amplify.DataStore.*` call with the equivalent Apollo iOS operation. The examples below use the schema and GraphQL operations created in the [Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/) page. + + + +## Create, update, and delete (save/delete) + + + +In Apollo Kotlin, you create, update, and delete data using the [mutations](https://www.apollographql.com/docs/kotlin/essentials/mutations) defined in your schema. + +```kotlin +// Create — include all required fields from your schema +val input = CreatePostInput( + title = title, + content = content, + rating = Optional.present(rating), + status = status +) +val response = apolloClient.mutation(CreatePostMutation(input)).execute() + +// Update — use Optional.present() for fields you want to change. +// Pass _version from the last query/mutation response for conflict resolution. +val input = UpdatePostInput( + id = post.id, + status = Optional.present(PostStatus.ACTIVE), + _version = Optional.present(post._version) +) +val response = apolloClient.mutation(UpdatePostMutation(input)).execute() + +// Delete — also requires _version for conflict resolution +val input = DeletePostInput( + id = post.id, + _version = Optional.present(post._version) +) +val response = apolloClient.mutation(DeletePostMutation(input)).execute() +``` + + + + + +In Apollo iOS, you create, update, and delete data using the [mutations](https://www.apollographql.com/docs/ios/fetching/mutations) defined in your schema. + +```swift +// Create — include all required fields from your schema +let createPostInput = CreatePostInput( + title: "post", + status: .some(.case(.active)), + rating: .some(5), + content: .some("content")) + +apolloClient.perform( + mutation: CreatePostMutation(input: createPostInput) +) { result in + guard let data = try? result.get().data else { return } + print("Post: \(String(describing: data.createPost?.fragments.postDetails))") +} + +// Update — use .some() for fields you want to change +let updatePostInput = UpdatePostInput( + id: "post1", + status: .some(.case(.inactive))) + +apolloClient.perform( + mutation: UpdatePostMutation(input: updatePostInput) +) { result in + guard let data = try? result.get().data else { return } + print("Post updated: \(String(describing: data.updatePost?.fragments.postDetails))") +} + +// Delete +let deletePostInput = DeletePostInput(id: "post1") +apolloClient.perform( + mutation: DeletePostMutation(input: deletePostInput) +) { result in + guard let data = try? result.get().data else { return } + print("Post deleted: \(String(describing: data.deletePost?.id))") +} +``` + + + +**`GraphQLNullable`:** Apollo iOS uses a `GraphQLNullable` type for optional input fields. This type distinguishes between "not provided" (`.none`), "explicitly null" (`.null`), and "has a value" (`.some(value)`). When setting optional fields with runtime values, you must use `.some(value)` explicitly — for example, `.some(.case(.active))` for enums or `.some(5)` for integers. + + + + + + + +**Include all required fields in creates.** If your schema defines `content: String!`, you must pass `content` in `CreatePostInput` — Apollo code generation makes it a required constructor parameter and the mutation will fail without it. + + + + + +**`_version` is required for updates and deletes.** If your AppSync API uses DataStore conflict resolution, you must pass the `_version` field when updating or deleting items. The `_version` is returned from every query and mutation response (via the `PostDetails` fragment). Always store the latest `_version` and pass it in subsequent mutations. If you omit `_version`, the mutation will fail with a `ConflictUnhandled` error. + + + +## Query + + + +In Apollo Kotlin, you query data using the [queries](https://www.apollographql.com/docs/kotlin/essentials/queries) defined in your schema. + +```kotlin +// Single item +val query = GetPostQuery(id = id) +val response = apolloClient.query(query).execute() + +// List items +val response = apolloClient.query(GetPostsQuery()).execute() +``` + + + + + +In Apollo iOS, you query data using the [queries](https://www.apollographql.com/docs/ios/fetching/queries) defined in your schema. + +```swift +// Single Item +let query = GetPostQuery(id: "post1") +apolloClient.fetch(query: query) { result in + guard let data = try? result.get().data else { return } + print("Query results: \(String(describing: data.getPost?.fragments.postDetails))") +} + +// List Items +let query = GetPostsQuery() +apolloClient.fetch(query: query) { result in + guard let data = try? result.get().data else { return } + print("List results: \(String(describing: data.listPosts?.items))") +} +``` + + + + + +**Soft-deleted items:** If your AppSync API uses DataStore conflict resolution, `listPosts` will return soft-deleted items with `_deleted: true`. DataStore filtered these out automatically, but with Apollo you **must filter them yourself** when displaying results: + +```kotlin +val posts = response.data?.listPosts?.items + ?.mapNotNull { it?.postDetails } + ?.filter { it._deleted != true } + ?: emptyList() +``` + +Failing to filter `_deleted` items will show deleted posts in your UI. + + + +### Filtering + +DataStore supported predicate queries (for example, `Post.RATING.gt(3)`) which were executed locally. With Apollo, you have two options: + +1. **Server-side filtering** using AppSync's `ModelPostFilterInput`. Update your `GetPosts` query to accept a filter: + +```graphql +query GetPosts($filter: ModelPostFilterInput, $nextToken: String) { + listPosts(filter: $filter, nextToken: $nextToken) { + items { ... PostDetails } + nextToken + } +} +``` + +Then pass the filter in Kotlin: + +```kotlin +val filter = ModelPostFilterInput( + rating = Optional.present(ModelIntInput(gt = Optional.present(3))) +) +val response = apolloClient.query( + GetPostsQuery(filter = Optional.present(filter)) +).execute() +``` + +2. **Client-side filtering** using Kotlin collection operations on the query results: + +```kotlin +val allPosts = response.data?.listPosts?.items + ?.mapNotNull { it?.postDetails } + ?: emptyList() +val filtered = allPosts.filter { (it.rating ?: 0) > 3 } +``` + +### Sorting + +DataStore's `QuerySortBy` does not have a direct Apollo equivalent for the default `listPosts` query. Apply sorting client-side: + +```kotlin +val sorted = posts.sortedByDescending { it.createdAt?.toString() ?: "" } +``` + +### Pagination + +DataStore supported offset-based pagination (`Page.startingAt(n).withLimit(5)`). AppSync uses cursor-based pagination with `nextToken`. You can either paginate through all results using `nextToken` or apply client-side offset pagination with `.drop()` and `.take()`: + +```kotlin +val page = 0 +val pageSize = 5 +val pagedPosts = allPosts.drop(page * pageSize).take(pageSize) +``` + +## Observe + +In Apollo Kotlin, you observe data using the [subscriptions](https://www.apollographql.com/docs/kotlin/essentials/subscriptions) defined in your schema. Apollo Kotlin exposes subscriptions as a Kotlin `Flow` via the built-in `toFlow()` method: + +```kotlin +apolloClient.subscription(OnCreateSubscription()).toFlow().collect { response -> + // Handle created post +} +``` + + + +Unlike DataStore's single `observe()` which emits create, update, and delete events through one stream, Apollo requires **separate subscriptions** for each event type (`onCreatePost`, `onUpdatePost`, `onDeletePost`). You need to launch each subscription flow independently. + + + +## ObserveQuery + +DataStore's `observeQuery` allows you to submit a query and then receive updates when the results change. To replicate this behavior with Apollo Kotlin, you use a [normalized cache](https://www.apollographql.com/docs/kotlin/caching/normalized-cache) and the [watch](https://www.apollographql.com/docs/kotlin/caching/query-watchers) function. + +To set this up, you need to: + +1. **Add the normalized cache dependency** to your `build.gradle.kts`: + +```kotlin +implementation("com.apollographql.apollo:apollo-normalized-cache:4.1.0") +// For SQLite-backed persistent cache: +implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.1.0") +``` + +2. **Configure the cache** when building your Apollo client: + +```kotlin +val cacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) +// Or for SQLite: SqlNormalizedCacheFactory(context, "apollo_cache.db") + +val apolloClient = ApolloClient.Builder() + .serverUrl(endpoint.serverUrl.toString()) + .addHttpInterceptor(AppSyncInterceptor(authorizer)) + .normalizedCache(cacheFactory) + .build() +``` + +3. **Watch the query** to receive updates: + +```kotlin +val query = GetPostQuery(id = id) +apolloClient.query(query).watch().collect { post -> + // This re-emits whenever the cached data for this query changes +} +``` + + + +The normalized cache tracks data by unique IDs, so mutations and subscription updates that write to the cache will automatically trigger `watch()` to re-emit with updated data. + + + + + +**No `isSynced` equivalent:** DataStore's `observeQuery()` provided an `isSynced` boolean indicating whether the local store was fully synced with the cloud. Apollo's `watch()` does not provide an equivalent sync status indicator. If your app relies on showing sync status, you need to track this manually (for example, by setting a flag after your initial query completes and subscriptions are established). + + + +## Quick reference table + +| DataStore Method | Apollo Kotlin Equivalent | Key Difference | +|---|---|---| +| `Amplify.DataStore.save()` (create) | `apolloClient.mutation(CreatePostMutation(input)).execute()` | No `_version` needed for creates | +| `Amplify.DataStore.save()` (update) | `apolloClient.mutation(UpdatePostMutation(input)).execute()` | Must pass `_version` from last query | +| `Amplify.DataStore.delete()` | `apolloClient.mutation(DeletePostMutation(input)).execute()` | Must pass `_version` from last query | +| `Amplify.DataStore.query()` (single) | `apolloClient.query(GetPostQuery(id)).execute()` | Returns null if not found | +| `Amplify.DataStore.query()` (list) | `apolloClient.query(GetPostsQuery()).execute()` | Must filter `_deleted` records | +| `Amplify.DataStore.observe()` | `apolloClient.subscription(...).toFlow().collect {}` | Separate subscription per event type | +| `Amplify.DataStore.observeQuery()` | `apolloClient.query(...).watch().collect {}` | Requires normalized cache setup | +| `Amplify.DataStore.clear()` | No longer needed | No local DataStore to clear | diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/index.mdx new file mode 100644 index 00000000000..d4232702cf2 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/index.mdx @@ -0,0 +1,462 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Migrate to Amplify API', + description: 'Replace DataStore save, query, delete, observe, and observeQuery with Amplify API equivalents for Flutter.', + platforms: [ + 'flutter' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +Amplify DataStore is built on top of the API category, using GraphQL queries, mutations, and subscriptions to interact with AWS AppSync. When migrating from DataStore, the API category provides the core building blocks to replicate DataStore's remote sync functionality. This page covers how to migrate every DataStore operation to its Amplify API equivalent. + +## Handle API responses + +A key difference between DataStore and the API category is the response type. DataStore methods return model objects or lists directly, while API methods return `GraphQLResponse` wrappers that may contain errors. + +```dart +// API query returns GraphQLResponse>, not List +final request = ModelQueries.list(Todo.classType); +final response = await Amplify.API.query(request: request).response; + +// Always check for errors +if (response.hasErrors) { + safePrint('Errors: ${response.errors}'); + return; +} + +// Access items from PaginatedResult — items may contain nulls +final todos = response.data?.items.nonNulls.toList() ?? []; +``` + +**Error handling patterns:** + +| Scenario | How to Detect | Action | +|---|---|---| +| Network error | `try/catch` around API call | Show offline message, retry later | +| GraphQL validation error | `response.hasErrors == true` and `response.data == null` | Fix request or show error | +| Partial success | `response.hasErrors == true` and `response.data != null` | Use data, log errors | +| Success | `response.hasErrors == false` and `response.data != null` | Use data | + +## Create and update (save) + +DataStore's `save()` handled both creating new items and updating existing ones. With the API category, you use separate `ModelMutations.create()` and `ModelMutations.update()` methods. + +**Before (DataStore):** + +```dart +// Create or update — DataStore decides based on whether the item exists +await Amplify.DataStore.save(todo); +``` + +**After (API) — create:** + +```dart +// Create — no _version needed, AppSync sets it to 1 automatically +final todo = Todo(name: 'my first todo', description: 'todo description'); +final request = ModelMutations.create(todo); +final response = await Amplify.API.mutate(request: request).response; + +if (response.hasErrors) { + safePrint('Create failed: ${response.errors}'); +} else { + safePrint('Created: ${response.data}'); +} +``` + +**After (API) — update:** + +```dart +// Update — the model instance carries _version internally. +// Always use a freshly queried instance to avoid ConditionalCheckFailedException. +final todoWithNewName = originalTodo.copyWith(name: 'new name'); +final updateRequest = ModelMutations.update(todoWithNewName); +final updateResponse = + await Amplify.API.mutate(request: updateRequest).response; + +if (updateResponse.hasErrors) { + safePrint('Update failed: ${updateResponse.errors}'); +} else { + safePrint('Updated: ${updateResponse.data}'); +} +``` + + + +**`_version` is required for updates.** The Amplify Flutter model objects carry `_version` internally, so `ModelMutations.update()` includes it automatically — but only if the model instance has the current version. Always query the record first to get the latest `_version`. If you see `ConditionalCheckFailedException`, you are passing a stale `_version`. + + + +## Delete + +DataStore's `delete()` method removes an item by passing the model instance. The API equivalent uses `ModelMutations.delete()`. When conflict resolution is enabled, delete is a **soft delete** — the record's `_deleted` field is set to `true` in DynamoDB, but the record is not physically removed. + +**Before (DataStore):** + +```dart +await Amplify.DataStore.delete(todo); +``` + +**After (API):** + +```dart +final request = ModelMutations.delete(todo); +final response = await Amplify.API.mutate(request: request).response; + +if (response.hasErrors) { + safePrint('Delete failed: ${response.errors}'); +} else { + safePrint('Deleted: ${response.data}'); +} +``` + +You can also delete by ID using `ModelMutations.deleteById()`: + +```dart +final request = ModelMutations.deleteById( + Todo.classType, + TodoModelIdentifier(id: todoId), +); +final response = await Amplify.API.mutate(request: request).response; +``` + + + +**`_version` is required for deletes.** Like updates, the model instance must carry the current `_version`. Query the record first if you are not sure you have the latest version. + + + +## Soft deletes and `_deleted` + +When conflict resolution is enabled, deletes are **soft deletes** — the record's `_deleted` field is set to `true` in DynamoDB, but the record is not physically removed. DataStore filtered these automatically; the API category does not. This means `ModelQueries.list()` returns soft-deleted records alongside active ones. + +The Amplify Flutter codegen models do not expose `_deleted` as a Dart property by default. To enable client-side filtering, add `_deleted` to your models in `schema.graphql` and re-run `amplify codegen models`: + +```graphql +type Todo @model { + id: ID! + name: String! + description: String + _deleted: Boolean +} +``` + +Then filter in Dart on every list query: + +```dart +final todos = response.data?.items.nonNulls + .where((t) => t.deleted != true) + .toList() ?? []; +``` + +Once you [disable conflict resolution](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/disable-conflict-resolution/) as the final migration step, soft deletes stop and this filtering is no longer needed. + +## Query + +DataStore's `query()` method supports query predicates, sorting, and page-based pagination. The API category supports query predicates through the `where` parameter but handles sorting and pagination differently. + +**Before (DataStore):** + +```dart +final todos = await Amplify.DataStore.query( + Todo.classType, + where: Todo.NAME.contains('important'), + sortBy: [Todo.CREATEDAT.descending()], + pagination: QueryPagination(page: 0, limit: 10), +); +// Returns List directly +``` + +**After (API) — single item:** + +```dart +final request = ModelQueries.get( + Todo.classType, + queriedTodo.modelIdentifier, +); +final response = await Amplify.API.query(request: request).response; +final todo = response.data; // Todo? — may be null +``` + +**After (API) — list with predicates:** + +```dart +final request = ModelQueries.list( + Todo.classType, + where: Todo.NAME.contains('important'), +); +final response = await Amplify.API.query(request: request).response; +final todos = response.data?.items.nonNulls + .where((t) => t.deleted != true) + .toList() ?? []; +``` + +### Sorting + +The API category's `ModelQueries.list()` does not support a `sortBy` parameter. You must implement sorting on the client side after receiving results: + +```dart +final request = ModelQueries.list(Todo.classType); +final response = await Amplify.API.query(request: request).response; +final todos = response.data?.items.nonNulls + .where((t) => t.deleted != true) + .toList() ?? []; + +// Client-side sort (e.g., by creation date, newest first) +todos.sort((a, b) { + final aDate = a.createdAt?.getDateTimeInUtc() ?? DateTime(0); + final bDate = b.createdAt?.getDateTimeInUtc() ?? DateTime(0); + return bDate.compareTo(aDate); +}); +``` + +### Pagination + +DataStore uses page-based pagination (`QueryPagination(page: N, limit: M)`), while the API category uses **token-based pagination** via `nextToken` on `PaginatedResult`. To paginate through all results: + +```dart +List allTodos = []; +GraphQLRequest> request = ModelQueries.list( + Todo.classType, + limit: 100, +); + +while (true) { + final response = await Amplify.API.query(request: request).response; + final page = response.data; + if (page == null) break; + + allTodos.addAll(page.items.nonNulls); + + if (page.hasNextResult) { + request = page.requestForNextResult!; + } else { + break; + } +} +``` + +If you need page-based pagination in your UI, you can implement it on the client side by fetching all items and then slicing: + +```dart +final pageSize = 10; +final pageIndex = 0; // zero-based + +// Fetch all (or use token-based pagination to fetch enough) +final allTodos = await _fetchAllTodos(); + +// Slice for display +final pageItems = allTodos.skip(pageIndex * pageSize).take(pageSize).toList(); +final totalPages = (allTodos.length / pageSize).ceil(); +``` + +## Observe + +DataStore's `observe()` returns a single stream with events that include an `EventType` (create, update, delete). The API category uses three separate subscription streams — one each for `onCreate`, `onUpdate`, and `onDelete`. + +**Before (DataStore):** + +```dart +final stream = Amplify.DataStore.observe(Todo.classType); +stream.listen((event) { + switch (event.eventType) { + case EventType.create: + safePrint('Created: ${event.item}'); + break; + case EventType.update: + safePrint('Updated: ${event.item}'); + break; + case EventType.delete: + safePrint('Deleted: ${event.item}'); + break; + } +}); +``` + +**After (API) — three separate subscriptions:** + +```dart +late StreamSubscription> _createSub; +late StreamSubscription> _updateSub; +late StreamSubscription> _deleteSub; + +void _initSubscriptions() { + // onCreate + final onCreateRequest = ModelSubscriptions.onCreate(Todo.classType); + _createSub = Amplify.API + .subscribe(onCreateRequest, + onEstablished: () => + safePrint('onCreate subscription established')) + .listen( + (event) { + safePrint('Created: ${event.data}'); + }, + onError: (Object e) => safePrint('onCreate error: $e'), + ); + + // onUpdate + final onUpdateRequest = ModelSubscriptions.onUpdate(Todo.classType); + _updateSub = Amplify.API + .subscribe(onUpdateRequest, + onEstablished: () => + safePrint('onUpdate subscription established')) + .listen( + (event) { + safePrint('Updated: ${event.data}'); + }, + onError: (Object e) => safePrint('onUpdate error: $e'), + ); + + // onDelete + final onDeleteRequest = ModelSubscriptions.onDelete(Todo.classType); + _deleteSub = Amplify.API + .subscribe(onDeleteRequest, + onEstablished: () => + safePrint('onDelete subscription established')) + .listen( + (event) { + safePrint('Deleted: ${event.data}'); + }, + onError: (Object e) => safePrint('onDelete error: $e'), + ); +} + +@override +void dispose() { + _createSub.cancel(); + _updateSub.cancel(); + _deleteSub.cancel(); + super.dispose(); +} +``` + + + +Unlike DataStore's single `observe()` stream, you now manage three separate subscriptions. Remember to cancel all three when your widget or controller is disposed. + + + +## ObserveQuery + +DataStore's `observeQuery()` combines an initial query with real-time updates, returning a `QuerySnapshot` that contains the full list of matching items and an `isSynced` flag. There is no direct equivalent in the API category — you must compose this behavior yourself using an initial list query plus subscriptions. + +**Before (DataStore):** + +```dart +final stream = Amplify.DataStore.observeQuery( + Todo.classType, + where: Todo.STATUS.eq(TodoStatus.ACTIVE), + sortBy: [Todo.CREATEDAT.descending()], +); + +stream.listen((QuerySnapshot snapshot) { + safePrint( + 'Items: ${snapshot.items.length}, isSynced: ${snapshot.isSynced}'); + setState(() { + _todos = snapshot.items; + }); +}); +``` + +**After (API) — initial query + subscriptions for reactive updates:** + +```dart +List _todos = []; +bool _isLoading = true; +late StreamSubscription _createSub; +late StreamSubscription _updateSub; +late StreamSubscription _deleteSub; + +Future _initObserveQuery() async { + // Step 1: Perform initial list query + await _refreshList(); + + // Step 2: Subscribe to changes and refresh on each event + final onCreate = ModelSubscriptions.onCreate(Todo.classType); + _createSub = + Amplify.API.subscribe(onCreate).listen((_) => _refreshList()); + + final onUpdate = ModelSubscriptions.onUpdate(Todo.classType); + _updateSub = + Amplify.API.subscribe(onUpdate).listen((_) => _refreshList()); + + final onDelete = ModelSubscriptions.onDelete(Todo.classType); + _deleteSub = + Amplify.API.subscribe(onDelete).listen((_) => _refreshList()); +} + +Future _refreshList() async { + final request = ModelQueries.list( + Todo.classType, + where: Todo.STATUS.eq(TodoStatus.ACTIVE), + ); + final response = await Amplify.API.query(request: request).response; + + if (response.hasErrors) { + safePrint('Query errors: ${response.errors}'); + return; + } + + final items = response.data?.items.nonNulls + .where((t) => t.deleted != true) + .toList() ?? []; + + // Client-side sort (API does not support sortBy) + items.sort((a, b) { + final aDate = a.createdAt?.getDateTimeInUtc() ?? DateTime(0); + final bDate = b.createdAt?.getDateTimeInUtc() ?? DateTime(0); + return bDate.compareTo(aDate); + }); + + setState(() { + _todos = items; + _isLoading = false; + }); +} + +@override +void dispose() { + _createSub.cancel(); + _updateSub.cancel(); + _deleteSub.cancel(); + super.dispose(); +} +``` + + + +DataStore's `QuerySnapshot.isSynced` flag is not available through the API category. If you relied on this flag to show sync status in your UI, you will need to track loading state manually (as shown with `_isLoading` above). + + + + + +**Performance tip:** For an optimized approach, instead of re-querying the entire list on every subscription event, you can update the local list in-place by adding, updating, or removing the item from the subscription event data. This avoids unnecessary network calls but requires more code to manage the list state. + + + +## Quick reference table + +| DataStore Method | Amplify API Equivalent | Key Difference | +|---|---|---| +| `Amplify.DataStore.save()` (create) | `Amplify.API.mutate(request: ModelMutations.create(...))` | Must check `response.hasErrors`; no `_version` needed for creates | +| `Amplify.DataStore.save()` (update) | `Amplify.API.mutate(request: ModelMutations.update(...))` | Must query first to get latest `_version`; model carries it internally | +| `Amplify.DataStore.delete()` | `Amplify.API.mutate(request: ModelMutations.delete(...))` | Must query first to get latest `_version`; delete is a soft delete when conflict resolution is enabled | +| `Amplify.DataStore.query()` (single) | `Amplify.API.query(request: ModelQueries.get(...))` | Returns `GraphQLResponse` wrapper | +| `Amplify.DataStore.query()` (list) | `Amplify.API.query(request: ModelQueries.list(...))` | Token-based pagination, no `sortBy`; returns soft-deleted records | +| `Amplify.DataStore.observe()` | `Amplify.API.subscribe(ModelSubscriptions.onCreate/onUpdate/onDelete(...))` | Three separate subscriptions | +| `Amplify.DataStore.observeQuery()` | Initial list query + three subscriptions | No direct equivalent; compose manually | +| `Amplify.DataStore.clear()` | No longer needed | No local DataStore to clear | diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/index.mdx new file mode 100644 index 00000000000..81bf6592e19 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/offline-first/index.mdx @@ -0,0 +1,393 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Offline-first', + description: 'Build full offline support with remote sync, live syncing, network detection, and offline mutations.', + platforms: [ + 'android', + 'flutter', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +For a better understanding of Amplify DataStore's opinionated approach, consult these resources: + + + +- [How DataStore works](https://docs.amplify.aws/gen1/flutter/build-a-backend/more-features/datastore/how-it-works/) +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) +- [Schema updates](https://docs.amplify.aws/gen1/flutter/build-a-backend/more-features/datastore/schema-updates/) + + + + + +- [How DataStore works](https://docs.amplify.aws/gen1/android/build-a-backend/more-features/datastore/how-it-works/) +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) +- [Schema updates](https://docs.amplify.aws/gen1/android/build-a-backend/more-features/datastore/schema-updates/) + + + + + +- [How DataStore works](https://docs.amplify.aws/gen1/swift/build-a-backend/more-features/datastore/how-it-works/) +- [Conflict detection and resolution](https://docs.aws.amazon.com/appsync/latest/devguide/conflict-detection-and-sync.html) +- [Schema updates](https://docs.amplify.aws/gen1/swift/build-a-backend/more-features/datastore/schema-updates/) + + + +To further explore and understand the principles of building offline-first applications, we recommend reviewing the [Android guide for building offline-first apps](https://developer.android.com/topic/architecture/data-layer/offline-first). + +We understand the importance of these capabilities to your applications, and while DataStore will no longer receive new features, we want to ensure that you have the necessary resources to transition smoothly. The following sections provide high-level recommendations on alternative solutions. + +## Remote sync + +By regularly synchronizing your local store, it can serve as the primary source for your application, allowing for read operations that can be done offline. Opting to manage offline writes will introduce additional complexity to your application, so it is crucial to consider whether you want to handle this for each individual model. + +## Live syncing + + + +You can achieve real-time detection of changes to a remote repository by leveraging `Amplify.API.subscribe` with `onCreate`, `onUpdate`, and `onDelete`, as long as you are connected to the internet. This method is not sufficient on its own since the subscriptions do not activate when the app is offline. + +```dart +Future initOnlineSync() async { + final onCreate = ModelSubscriptions.onCreate(RemoteModel.classType); + final createSubscription = Amplify.API.subscribe(onCreate); + createSubscription.listen((response) { + _syncItem(response, localRepository.create); + }); + + final onUpdate = ModelSubscriptions.onUpdate(RemoteModel.classType); + final updateSubscription = Amplify.API.subscribe(onUpdate); + updateSubscription.listen((response) { + _syncItem(response, localRepository.update); + }); + + final onDelete = ModelSubscriptions.onDelete(RemoteModel.classType); + final deleteSubscription = Amplify.API.subscribe(onDelete); + deleteSubscription.listen((response) { + _syncItem(response, localRepository.delete); + }); +} + +Future _syncItem( + GraphQLResponse response, + Function(LocalModel) syncFunction, +) async { + final remoteModel = response.data; + if (remoteModel == null) return; + + final localModel = toLocalModel(remoteModel); + // Calls localRepository's create, update, or delete method + syncFunction.call(localModel); +} +``` + + + + + +You can achieve real-time detection of changes (create, update, delete) to a remote repository by leveraging AWS AppSync subscriptions, as long as you are connected to the internet. The following function subscribes to remote creation, updates, and deletion and propagates those changes into your Room database. These subscriptions will need to be established each time your application reconnects to the internet. + +```kotlin +suspend fun startSubscriptions() = supervisorScope { + // Subscribe to creates + apolloClient.subscription(OnCreateSubscription()).toFlow().onEach { response -> + response.data?.onCreatePost?.postDetails?.let { post -> + postDao.insert(post.toLocalModel()) + } + }.launchIn(this) + + // Subscribe to updates + apolloClient.subscription(OnUpdateSubscription()).toFlow().onEach { response -> + response.data?.onUpdatePost?.postDetails?.let { post -> + postDao.insert(post.toLocalModel()) + } + }.launchIn(this) + + // Subscribe to deletes + apolloClient.subscription(OnDeleteSubscription()).toFlow().onEach { response -> + response.data?.onDeletePost?.postDetails?.let { post -> + postDao.delete(post.toLocalModel()) + } + }.launchIn(this) +} +``` + + + +## Local cache refresh + +Whenever your app reconnects to the internet, whether through launching the app or network updates, perform a complete refresh of your local store to ensure that you receive all the updates that you may have missed. + + + +```dart +Future _syncAll() async { + final response = await remoteRepository.readAll(); + final remoteItems = response.data?.items; + final localData = remoteItems?.nonNulls.map(toLocalModel); + + if (localData == null) return; + + // Clear local and save results + localRepository.rebuild(localData); +} +``` + + + + + +```kotlin +suspend fun syncAllPosts() { + // Sync all pages of posts until there is no nextToken + var nextToken: String? = null + do { + val query = GetPostsQuery(nextToken = Optional.presentIfNotNull(nextToken)) + val response = apolloClient.query(query).execute() + response.data?.listPosts?.items?.let { posts -> + val mapped = posts.mapNotNull { it?.postDetails?.toLocalModel() } + postDao.insertAll(mapped) + } + nextToken = response.data?.listPosts?.nextToken + } while (nextToken != null) +} +``` + +If you often sync large amounts of data, configuring AWS AppSync Delta Sync operations can reduce the amount of time it takes to refresh your local store. Read more about how to configure and implement Delta Sync functionality in the [AWS AppSync Delta Sync guide](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-delta-sync.html). + + + +## Detect network status + + + +You can reactively detect network status updates by waiting for an `Amplify.API.mutate` operation to throw an exception or for your subscription observers to send an error. + +```dart +void _initObservers() { + final onCreate = ModelSubscriptions.onCreate(Todo.classType); + Amplify.API + .subscribe(onCreate) + .listen(_syncCreate, onError: (_) => _offlineDetected()); + + final onUpdate = ModelSubscriptions.onUpdate(Todo.classType); + Amplify.API + .subscribe(onUpdate) + .listen(_syncUpdate, onError: (_) => _offlineDetected()); + + final onDelete = ModelSubscriptions.onDelete(Todo.classType); + Amplify.API + .subscribe(onDelete) + .listen(_syncDelete, onError: (_) => _offlineDetected()); +} + +Future> create(Todo model) async { + try { + final request = ModelMutations.create(model); + return Amplify.API.mutate(request: request).response; + } catch (e, st) { + _offlineDetected(); + rethrow; + } +} +``` + +Additionally, you can proactively detect network status updates via Amplify Hub. This allows you to identify scenarios where your local cache needs to be synced even though you are not actively making network requests. + +```dart +Amplify.Hub.listen(HubChannel.Api, _onNetworkStatusChanged); + +void _onNetworkStatusChanged(ApiHubEvent hubEvent) { + if (hubEvent is SubscriptionHubEvent) { + switch (hubEvent.status) { + case SubscriptionStatus.connected: + _onlineDetected(); + break; + case SubscriptionStatus.connecting: + case SubscriptionStatus.pendingDisconnected: + case SubscriptionStatus.disconnected: + case SubscriptionStatus.failed: + _offlineDetected(); + default: + break; + } + } +} +``` + +Once you have detected that the application is offline, periodically attempt to resync your local cache. This allows you to detect when your app is back online and cleans up any stale data. We recommend implementing a [backoff strategy](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) or using a third-party package to efficiently distribute your resync attempts. + +```dart +Future _offlineDetected() async { + if (_isOffline) return; + _isOffline = true; + + final backoffStrategy = ExponentialJitterBackoff(); + while (_isOffline) { + await Future.delayed(backoffStrategy.nextDelay()); + + _isOffline = !await _resync(); + } +} +``` + + + + + +You can proactively detect network status updates via Android's [ConnectivityManager](https://developer.android.com/training/monitoring-device-state/connectivity-status-type). This allows you to identify scenarios where your local cache needs to be synced and subscriptions need to be reestablished. + +```kotlin +class NetworkMonitor { + + enum class Status { + Connected, + Disconnected + } + + fun monitor(context: Context) = callbackFlow { + val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + trySend(Status.Connected) + } + + override fun onLost(network: Network) { + super.onLost(network) + trySend(Status.Disconnected) + } + } + + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() + + val connectivityManager = + context.getSystemService(ConnectivityManager::class.java) + as ConnectivityManager + connectivityManager.requestNetwork(networkRequest, networkCallback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } +} + +class SyncEngine(val networkMonitor: NetworkMonitor) { + suspend fun start(context: Context) { + networkMonitor.monitor(context).collect { networkStatus -> + when (networkStatus) { + NetworkMonitor.Status.Connected -> startSync() + NetworkMonitor.Status.Disconnected -> stopSync() + } + } + } +} +``` + + + +## Offline mutations + +By storing any changes made locally in a separate pending mutations table and synchronizing them at a later time, you can enable your users to work offline. Your pending mutations table should contain: + +1. Auto incrementing primary key (not your model's normal primary key) +2. Enum for type of mutation (create, update, or delete) +3. The state of the model at the time of the action + + + +As you are syncing your pending mutations table, handle your `Amplify.API.mutate` responses per the following table: + +| Exception Was Thrown | Response Has Data | Response hasErrors | Result | Action | +|---|---|---|---|---| +| TRUE | N/A | N/A | Exception | Pause Syncing | +| FALSE | TRUE | FALSE | Success | Continue Syncing | +| FALSE | TRUE | TRUE | Partial Success | Continue Syncing | +| FALSE | FALSE | TRUE | Validation Error | Continue Syncing | +| FALSE | FALSE | FALSE | Not a Valid Result | N/A | + +To incorporate pending mutations table entries into your app before syncing, you must aggregate them with the results of your local store queries. + +## Manually start and stop sync + +DataStore allows for syncing of data to be manually stopped and started. To implement this, you can check if syncing is enabled before running the sync logic and provide a way to toggle the flag. Remember to sync your local store when enabling sync. + +```dart +bool _syncEnabled = true; +void enableSync() { + final previous = _syncEnabled; + _syncEnabled = true; + + if (previous != _syncEnabled) _syncAll(); +} + +void disableSync() { + _syncEnabled = false; +} +``` + + + + + + + +Offline mutations can introduce significant complexity to your application. Synchronization, reconciliation, and conflict resolution can each be a non-trivial problem. You should carefully consider the pros and cons before deciding to support offline mutations. + + + +As an example, a pending mutation for the sample `Post` type might look like the following: + +```kotlin +enum class MutationType { + Create, + Update, + Delete +} + +@Entity(tableName = "post_mutation") +data class PostMutationEntity( + val postId: String, + val type: MutationType, + val title: String?, + val content: String?, + val status: PostStatus?, + val rating: Int?, + val timestamp: String, + @PrimaryKey(autoGenerate = true) val id: Int = 0 +) +``` + +### Apply pending mutations + +These pending mutations can be synced to AWS AppSync when the application has connectivity. To incorporate pending mutation table entries into your app before syncing, you must aggregate them with the results of your local store queries. You can fetch both the `PostEntity` and `PostMutationEntity` instances, and conflate the two into a single display model, optimistically applying the mutation before it is processed by AWS AppSync. + +```kotlin +fun PostMutationEntity.applyTo(post: PostEntity) = post.copy( + title = title ?: post.title, + content = content ?: post.content, + status = status ?: post.status, + rating = rating ?: post.rating +) +``` + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/index.mdx new file mode 100644 index 00000000000..557317fbec6 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/remove-datastore/index.mdx @@ -0,0 +1,231 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Remove DataStore', + description: 'Remove Amplify DataStore from your project and prepare for migration to the Amplify API category or Apollo Kotlin.', + platforms: [ + 'android', + 'flutter', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + + + +Before migrating your DataStore calls, remove Amplify DataStore from your project and configure the API plugin to use your models. + +## Update dependencies + +Remove the `amplify_datastore` package from your `pubspec.yaml` and ensure the `amplify_api` package is present: + +```yaml +dependencies: + # Remove this line: + # amplify_datastore: ^2.x.x + + # Keep or add these: + amplify_flutter: ^2.x.x + amplify_api: ^2.x.x + amplify_auth_cognito: ^2.x.x # if using auth +``` + +After updating `pubspec.yaml`, run: + +```bash +flutter pub get +``` + +## Update Amplify configuration + +When using DataStore, the `ModelProvider` was passed to the `AmplifyDataStore` plugin. After removing DataStore, you **must** pass `ModelProvider.instance` to the `AmplifyAPI` plugin instead. Without this, `ModelMutations` and `ModelQueries` cannot serialize and deserialize your models, and API calls will fail at runtime. + +**Before (with DataStore):** + +```dart +import 'package:amplify_datastore/amplify_datastore.dart'; +import 'package:amplify_api/amplify_api.dart'; +import 'models/ModelProvider.dart'; + +final datastorePlugin = AmplifyDataStore(modelProvider: ModelProvider.instance); +final api = AmplifyAPI(); + +await Amplify.addPlugins([datastorePlugin, api]); +await Amplify.configure(amplifyConfig); +``` + +**After (API only):** + +```dart +import 'package:amplify_api/amplify_api.dart'; +import 'package:amplify_api/model_queries.dart'; +import 'package:amplify_api/model_mutations.dart'; +import 'package:amplify_api/model_subscriptions.dart'; +import 'models/ModelProvider.dart'; + +// Pass ModelProvider to the API plugin +final api = AmplifyAPI( + options: APIPluginOptions(modelProvider: ModelProvider.instance), +); + +await Amplify.addPlugins([api]); +await Amplify.configure(amplifyConfig); +``` + + + +If you forget to pass `ModelProvider.instance` to `AmplifyAPI`, model-based queries and mutations will not work. This is the most common migration mistake. + + + +## Remove all DataStore calls + +Search your codebase and remove or replace all DataStore calls: + +| DataStore Call | API Replacement | +|---|---| +| `Amplify.DataStore.save()` | `Amplify.API.mutate()` with `ModelMutations.create()` / `.update()` | +| `Amplify.DataStore.delete()` | `Amplify.API.mutate()` with `ModelMutations.delete()` | +| `Amplify.DataStore.query()` | `Amplify.API.query()` with `ModelQueries.get()` / `.list()` | +| `Amplify.DataStore.observe()` | `Amplify.API.subscribe()` with `ModelSubscriptions.onCreate/onUpdate/onDelete()` | +| `Amplify.DataStore.observeQuery()` | Initial list query + three subscriptions (see [Migrate to Amplify API](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-to-api/#observequery)) | +| `Amplify.DataStore.clear()` | No longer needed (no local DataStore to clear) | +| `Amplify.DataStore.start()` / `stop()` | No longer needed | + + + +Keep `amplify_auth_cognito` (or other auth plugins) if you still use Amplify for authentication or other services. You only need to remove the DataStore plugin and dependency. + + + + + +**`Amplify.DataStore.clear()` in sign-out flows:** If your sign-out flow calls `Amplify.DataStore.clear()` to wipe local data before signing out, remove that call entirely — there is no local DataStore to clear after migration. + + + + + + + +Before adding Apollo, remove Amplify DataStore from your project. + +## Remove DataStore frameworks from your target + +1. In the Xcode **Navigator panel** (left sidebar), select the **blue project icon** at the very top (the document icon labeled with your project name). This opens the **Project Editor**. +2. In the Project Editor, you will see two columns: **PROJECT** and **TARGETS**. Select your app name under **TARGETS** (not under PROJECT). +3. Select the **"General"** tab at the top. +4. Scroll down to the **"Frameworks, Libraries, and Embedded Content"** section (below "Supported Destinations" and "Minimum Deployments"). +5. Remove the DataStore-related frameworks by selecting each one and selecting the **⊖ (minus)** button at the bottom of the list. Remove at minimum: + - `AWSDataStorePlugin` + - `AWSAPIPlugin` (if it was only used by DataStore) + + + +A typical Amplify project may have many framework products linked beyond DataStore, such as `AWSCloudWatchLoggingPlugin`, `AWSLocationGeoPlugin`, `AWSPinpointAnalyticsPlugin`, `AWSPinpointPushNotificationsPlugin`, `AWSPredictionsPlugin`, `AWSS3StoragePlugin`, etc. Remove all frameworks that you are not actively using. **Keep** the frameworks you still need — for example, if you use Amplify for authentication, keep `Amplify`, `AWSCognitoAuthPlugin`, `AWSPluginsCore`, and `Authenticator` (if using the Amplify Authenticator SwiftUI component). + + + +## Remove the DataStore plugin from app initialization + +Remove the DataStore and API plugin registration from your Amplify configuration code: + +```swift +// Remove these lines: +try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: AmplifyModels())) +try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels())) +``` + +Keep any other plugins you still need (e.g., `AWSCognitoAuthPlugin`). + +## Delete DataStore generated model files + +If you used Amplify codegen, delete the generated model classes. These are typically found in a `Models/` directory and include files like: +- `Post.swift` +- `Post+Schema.swift` +- `AmplifyModels.swift` + +These will be replaced by Apollo's generated types. + +## Remove all `Amplify.DataStore.*` calls + +Search your codebase and remove or replace all DataStore calls: + +| DataStore Call | Apollo Replacement | +|---|---| +| `Amplify.DataStore.save()` | Apollo mutations (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) | +| `Amplify.DataStore.delete()` | Apollo mutations (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) | +| `Amplify.DataStore.query()` | Apollo queries (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) | +| `Amplify.DataStore.observe()` | Apollo subscriptions (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/)) | +| `Amplify.DataStore.observeQuery()` | Apollo cache watchers (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/#observequery)) | +| `Amplify.DataStore.clear()` | `apolloClient.store.clearCache()` | +| `Amplify.DataStore.start()` / `.stop()` | No longer needed | + + + +Keep `AWSCognitoAuthPlugin` (or other auth plugins) if you still use Amplify for authentication or other services. You only need to remove the DataStore plugin and the API plugin (if only used by DataStore). If you use the Amplify `Authenticator` SwiftUI view, keep `Authenticator` and `AWSCognitoAuthPlugin` — they work independently of DataStore. + + + + + + + +Before adding Apollo, remove Amplify DataStore from your project. + +## Remove the DataStore dependency + +Remove `com.amplifyframework:aws-datastore` (or its version catalog alias) from your `build.gradle.kts` (or `libs.versions.toml`). + +## Remove the DataStore plugin + +Remove the DataStore plugin from your `Application` class: + +```kotlin +// Remove this line from your Amplify.addPlugin() calls: +Amplify.addPlugin(AWSDataStorePlugin()) +``` + +## Delete DataStore generated model files + +If you used Amplify codegen, delete the generated model classes (for example, `Post.java`, `PostStatus.java`, `AmplifyModelProvider.java` under `com/amplifyframework/datastore/generated/model/`). These will be replaced by Apollo's generated types. + +## Remove all `Amplify.DataStore.*` calls + +Remove all DataStore API calls throughout your code: + +| DataStore Call | Apollo Replacement | +|---|---| +| `Amplify.DataStore.save()` | Apollo mutations | +| `Amplify.DataStore.delete()` | Apollo mutations | +| `Amplify.DataStore.query()` | Apollo queries | +| `Amplify.DataStore.observe()` | Apollo subscriptions | +| `Amplify.DataStore.observeQuery()` | Apollo cache watchers (see [Migrate DataStore to Apollo](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/migrate-datastore-to-apollo/#observequery)) | +| `Amplify.DataStore.clear()` | No longer needed (no local DataStore to clear) | +| `Amplify.DataStore.start()` / `stop()` | No longer needed | + + + +Keep `AWSApiPlugin` and `AWSCognitoAuthPlugin` (or other auth plugins) if you still use Amplify for authentication or other services. You only need to remove the DataStore plugin and dependency. + + + + + +**`Amplify.DataStore.clear()` in sign-out flows:** If your sign-out flow calls `Amplify.DataStore.clear()` to wipe local data before signing out, remove that call entirely — there is no local DataStore to clear after migration. + + + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/index.mdx new file mode 100644 index 00000000000..8dada7f84cb --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/index.mdx @@ -0,0 +1,222 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Schema and GraphQL operations', + description: 'Retrieve your AppSync schema and define GraphQL operations for Apollo Kotlin code generation.', + platforms: [ + 'android', + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + + + +Apollo's code generation plugin reads your schema and GraphQL operation files to generate type-safe Kotlin code. This page covers how to retrieve your AppSync schema and define the GraphQL operations you need. + + + + + +The schema is used by Apollo's code generation tool to generate type-safe Swift code that helps you execute GraphQL operations. This page covers how to retrieve your AppSync schema and define the GraphQL operations you need. + + + +## Retrieve your AWS AppSync GraphQL schema + +### Using the AWS AppSync web console + +1. Navigate to your API on the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) +2. On the left side, select **Schema** +3. Select the **Export Schema** dropdown and download the `schema.json` file + + +4. Place this file at `app/src/main/graphql/schema.json` in your project (or the location configured in your Apollo Gradle plugin — see [Apollo documentation](https://www.apollographql.com/docs/kotlin/advanced/plugin-recipes#specifying-the-schema-location)) + + + + + +4. Place this file in your project directory (e.g., alongside your `.xcodeproj`). + + + +### Using the AWS CLI + +1. Run the following command with the [AWS CLI](https://aws.amazon.com/cli/) to download the schema: + +```bash +aws appsync get-introspection-schema \ + --api-id \ + --format JSON \ + --region \ + schema.json +``` + +You can find your API ID in the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) or by running: + +```bash +aws appsync list-graphql-apis --region +``` + + + +2. Place the downloaded `schema.json` at `app/src/main/graphql/schema.json` in your project. + + + + + +2. Place the downloaded `schema.json` in your project directory. + + + +## Prepare GraphQL operations (queries, mutations, subscriptions) + +### Import from Amplify CLI + +The Amplify CLI can auto-generate queries (`queries.graphql`), mutations (`mutations.graphql`), and subscriptions (`subscriptions.graphql`) for your Amplify API / AppSync projects. Follow the [Client code generation guide](https://docs.amplify.aws/gen1/android/tools/cli-legacy/client-codegen/) to generate these files. Once generated, add them to your project as directed by the [Apollo documentation](https://www.apollographql.com/docs/kotlin/advanced/plugin-recipes#specifying-the-schema-location). + + + +Using Amplify-generated files is a good way to get started, but we recommend writing your own queries to selectively limit the queried data to your use case and to use GraphQL fragments for model reuse across your application. + + + +### Write your own + +Place your GraphQL operations in `app/src/main/graphql/`, which is the default location the Apollo Gradle plugin looks for these files. When writing your own operations, the [AWS AppSync Query Editor](https://docs.aws.amazon.com/appsync/latest/devguide/console-tour.html#queries-editor) is helpful for testing. + +#### Fragment + +Create a [fragment](https://www.apollographql.com/docs/kotlin/essentials/fragments) to reuse in all of your operations: + +```graphql +# fragments.graphql + +fragment PostDetails on Post { + id + updatedAt + createdAt + title + content + status + rating + _version + _deleted + _lastChangedAt +} +``` + + + +**Include all fields your app uses.** Your fragment must include every field required by your mutations. For example, if `content` is a required field (`String!`) in your schema, you must include it in your fragment and mutation inputs — otherwise mutations will fail with a validation error. + + + + + +**DataStore conflict resolution fields:** If your AppSync API uses DataStore conflict resolution, your schema includes `_version`, `_deleted`, and `_lastChangedAt` fields. You **must** include `_version` in your fragment and pass it in update and delete mutations — otherwise mutations will fail with a `ConflictUnhandled` error. + + + +#### Mutations + +Create [mutations](https://www.apollographql.com/docs/kotlin/essentials/mutations) that modify or create posts. Using the `PostDetails` fragment returns the same data type in each mutation: + +```graphql +# mutations.graphql + +# Create +mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + ... PostDetails + } +} + +# Update +mutation UpdatePost($input: UpdatePostInput!) { + updatePost(input: $input) { + ... PostDetails + } +} + +# Delete +mutation DeletePost($input: DeletePostInput!) { + deletePost(input: $input) { + id + } +} +``` + +#### Queries + +Create [queries](https://www.apollographql.com/docs/kotlin/essentials/queries) that fetch `PostDetails` from AWS AppSync: + +```graphql +# queries.graphql + +# Single Item +query GetPost($id: ID!) { + getPost(id: $id) { + ... PostDetails + } +} + +# List Items +query GetPosts($nextToken: String) { + listPosts(nextToken: $nextToken) { + items { + ... PostDetails + } + nextToken + } +} +``` + +#### Subscriptions + +Create [subscriptions](https://www.apollographql.com/docs/kotlin/essentials/subscriptions) that notify the app when a post is created, updated, or deleted: + +```graphql +# subscriptions.graphql + +# Create +subscription onCreateSubscription { + onCreatePost { + ... PostDetails + } +} + +# Update +subscription onUpdateSubscription { + onUpdatePost { + ... PostDetails + } +} + +# Delete +subscription onDeleteSubscription { + onDeletePost { + id + } +} +``` + + + +**Generated class names:** Apollo Kotlin generates a class for each operation using the operation name from your `.graphql` file. For example, `subscription onCreateSubscription` generates a class named `OnCreateSubscription`. If you named it `subscription OnCreatePost`, the generated class would be `OnCreatePostSubscription` (Apollo appends `Subscription` / `Query` / `Mutation` to the class name if it is not already in the operation name). Check the generated code in `build/generated/source/apollo/` to confirm the class names. + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/index.mdx new file mode 100644 index 00000000000..90633cb6b5d --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-ios/index.mdx @@ -0,0 +1,358 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Set up Apollo iOS', + description: 'Add Apollo iOS dependencies, install the CLI, run code generation, and configure the Apollo client with your AppSync endpoint and authentication.', + platforms: [ + 'swift' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +Read the Apollo iOS [Getting Started section](https://www.apollographql.com/docs/ios/get-started) for information about adding Apollo iOS to your project. + +## Add dependencies + +Follow these steps to add the required packages in Xcode: + +### Step 1: Add the Apollo iOS package + +1. In Xcode, go to **File → Add Package Dependencies...** +2. In the dialog that appears, paste the following URL into the search field at the top right: + +``` +https://github.com/apollographql/apollo-ios.git +``` + +3. Wait for Xcode to fetch the package metadata (this may take 10–30 seconds). +4. **Set the version constraint:** Select the version dropdown and choose **"Up to Next Major Version"**. Set the range to start from **`1.0.0`** with an upper bound of **`2.0.0`**. This ensures you get Apollo iOS 1.x, which is required by the AWS AppSync Apollo Extensions library. **Do not use Apollo iOS 2.x.** +5. Select **"Add Package"**. +6. In the product selection dialog, check the **"Apollo"** checkbox. Make sure your app target is selected. Select **"Add Package"**. + +### Step 2: Add the AWS AppSync Apollo Extensions package + +1. Go to **File → Add Package Dependencies...** +2. Paste the following URL: + +``` +https://github.com/aws-amplify/aws-appsync-apollo-extensions-swift.git +``` + +3. Set the version to **"Up to Next Major Version"** from **`1.0.0`**. +4. Select **"Add Package"**, select **"AWSAppSyncApolloExtensions"**, and add it to your target. + +### Step 3: Keep Amplify Auth (if needed) + +If you use Amplify for authentication, your existing `amplify-swift` package should already be in your project. Make sure **`AWSPluginsCore`** is added to your target's **Frameworks, Libraries, and Embedded Content** — it is needed for `AuthCognitoTokensProvider` when configuring the Apollo client with Cognito auth. To check: + +1. Select the **blue project icon** in the Navigator → select your app under **TARGETS** → **General** tab. +2. Scroll to **Frameworks, Libraries, and Embedded Content**. +3. If `AWSPluginsCore` is not listed, select the **+ (plus)** button and select it from the list. + +### Summary of packages + +| Package | URL | Version | Product to Add | +|---|---|---|---| +| Apollo iOS | `https://github.com/apollographql/apollo-ios.git` | `1.0.0..<2.0.0` | `Apollo` | +| AWS AppSync Apollo Extensions | `https://github.com/aws-amplify/aws-appsync-apollo-extensions-swift.git` | `1.0.0..<2.0.0` | `AWSAppSyncApolloExtensions` | +| Amplify Swift (if using Amplify Auth) | `https://github.com/aws-amplify/amplify-swift.git` | Keep existing | `AWSPluginsCore` | + +## Install the Apollo iOS CLI + +The Apollo iOS CLI (`apollo-ios-cli`) is used for code generation. It is **not** available via Homebrew. You can obtain it in one of these ways: + +1. **Download from GitHub Releases** (recommended): Go to [Apollo iOS Releases](https://github.com/apollographql/apollo-ios/releases), find the release matching your Apollo iOS library version, and download the `apollo-ios-cli.tar.gz` asset from the release assets. + +After downloading: + +```bash +# Extract the binary from the archive +tar -xzf apollo-ios-cli.tar.gz + +# Make the binary executable (macOS may require this) +chmod +x apollo-ios-cli + +# Move it to your project directory (same level as apollo-codegen-config.json) +mv apollo-ios-cli /path/to/your/project/ +``` + +2. **Build from source**: Clone the `apollo-ios` repo at the matching tag and build the CLI. + + + +**Version matching is critical.** The Apollo iOS CLI version **must exactly match** your resolved Apollo iOS library version. A mismatched CLI version produces Swift files that are incompatible with the library, causing compile errors such as `Type 'PostDetails' does not conform to protocol 'SelectionSet'` on every generated type. + +**To check your resolved library version:** +- In Xcode: **File → Packages → Resolved Versions** (or check `Package.resolved` in your `.xcworkspace/xcshareddata/swiftpm/` directory) +- Look for the `apollo-ios` entry and note the exact version (e.g., `1.25.3`) +- Download the CLI release with that **exact same version number** + +**Recommended workflow:** +1. Add Apollo iOS package to your project (Step 1 above) +2. Let Xcode/SPM resolve the package version +3. Check the resolved version +4. Download the matching Apollo iOS CLI version +5. Then proceed to code generation + + + +## Code generation + +Place your `schema.json` and `.graphql` operation files in a `graphql/` subdirectory of your project (you should have done this in the [Schema and GraphQL operations](/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/schema-and-operations/) page). Your project directory should look like: + +``` +YourProject/ +├── YourProject.xcodeproj +├── schema.json +├── apollo-ios-cli +├── apollo-codegen-config.json +├── graphql/ +│ ├── fragments.graphql +│ ├── mutations.graphql +│ ├── queries.graphql +│ └── subscriptions.graphql +└── YourProject/ + └── (your Swift source files) +``` + +You can either initialize the config and modify it, or create `apollo-codegen-config.json` directly. Here is the configuration you need: + +```json +{ + "schemaNamespace": "ApolloCodeGen", + "input": { + "operationSearchPaths": [ + "./graphql/fragments.graphql", + "./graphql/mutations.graphql", + "./graphql/queries.graphql", + "./graphql/subscriptions.graphql" + ], + "schemaSearchPaths": [ + "./schema.json" + ] + }, + "output": { + "testMocks": { + "none": {} + }, + "schemaTypes": { + "path": "./ApolloCodeGen", + "moduleType": { + "swiftPackageManager": {} + } + }, + "operations": { + "inSchemaModule": {} + } + } +} +``` + + + +If you prefer, you can generate an initial config using `./apollo-ios-cli init --schema-namespace ApolloCodeGen --module-type swiftPackageManager` and then modify the `operationSearchPaths` and `schemaSearchPaths` to match the paths above. The default config uses `**/*.graphqls` for `schemaSearchPaths`, but AppSync exports the schema as `schema.json`, so you must update the path. + + + +Generate the Swift files: + +```bash +./apollo-ios-cli generate +``` + +This creates an `ApolloCodeGen` directory containing a Swift package with all generated types. + +## Add generated code to your Xcode project + +After code generation, add the generated Swift package to your project: + +1. In Xcode, go to **File → Add Package Dependencies...** +2. At the **bottom-left** of the dialog, select the **"Add Local..."** button (this is not the URL search field at the top). +3. In the file picker, navigate to and select the generated `ApolloCodeGen` directory inside your project. +4. Xcode will recognize it as a local Swift package. Select **"Add Package"**. +5. Make sure the **ApolloCodeGen** library product is added to your app target. If it does not appear automatically, go to your target's **General** tab → **Frameworks, Libraries, and Embedded Content** → select **+ (plus)** → select **ApolloCodeGen** from the list. + + + +**Generated types and namespacing:** All generated types live inside the `ApolloCodeGen` module. When referencing generated types in your Swift code (such as enums), you must use the fully qualified name `ApolloCodeGen.PostStatus` or add `import ApolloCodeGen` at the top of your file. This is different from Amplify DataStore's generated models, which lived in your app module directly. For example: + +```swift +// Before (DataStore): +let status: PostStatus = .active + +// After (Apollo): +let status: ApolloCodeGen.PostStatus = .case(.active) +// or: import ApolloCodeGen, then use GraphQLEnum +``` + + + +## Configure the Apollo client + +The runtime component of Apollo must be configured to connect to AWS AppSync, including handling authorization modes and the subscription protocol. The [AWS AppSync Apollo Extensions library](https://docs.amplify.aws/swift/build-a-backend/data/aws-appsync-apollo-extensions/) implements the required logic. + +### With Amplify Gen 2 config (`amplify_outputs.json`) + +If your project uses Amplify Gen 2 (or you have converted your Gen 1 config — see the note below), you can use `AWSAppSyncConfiguration` to read the endpoint and configure authorization automatically. + +**For API Key auth:** + +```swift +let store = ApolloStore(cache: InMemoryNormalizedCache()) +// 1. Read AWS AppSync API configuration from amplify_outputs.json +let configuration = try AWSAppSyncConfiguration(with: .amplifyOutputs) + +// 2. Use configuration.apiKey with APIKeyAuthorizer +let authorizer = APIKeyAuthorizer(apiKey: configuration.apiKey ?? "") +let interceptor = AppSyncInterceptor(authorizer) +let interceptorProvider = DefaultPrependInterceptorProvider( + interceptor: interceptor, + store: store) + +// 3. Use configuration.endpoint with RequestChainNetworkTransport +let transport = RequestChainNetworkTransport( + interceptorProvider: interceptorProvider, + endpointURL: configuration.endpoint) +let apolloClient = ApolloClient( + networkTransport: transport, + store: store) +``` + +**For Cognito User Pool auth (owner-based authorization):** + +```swift +import AWSPluginsCore + +let store = ApolloStore(cache: InMemoryNormalizedCache()) +let configuration = try AWSAppSyncConfiguration(with: .amplifyOutputs) + +let authorizer = AuthTokenAuthorizer { + let session = try await Amplify.Auth.fetchAuthSession() + if let tokenProvider = session as? AuthCognitoTokensProvider { + let tokens = try tokenProvider.getCognitoTokens().get() + return tokens.accessToken + } + throw AuthError.unknown("Unable to get Cognito tokens") +} + +let interceptor = AppSyncInterceptor(authorizer) +let interceptorProvider = DefaultPrependInterceptorProvider( + interceptor: interceptor, + store: store) +let transport = RequestChainNetworkTransport( + interceptorProvider: interceptorProvider, + endpointURL: configuration.endpoint) +let apolloClient = ApolloClient( + networkTransport: transport, + store: store) +``` + + + +`AuthCognitoTokensProvider` requires importing **`AWSPluginsCore`** from the `amplify-swift` package. Make sure to add this library product to your target. + + + + + +**Gen 1 to Gen 2 config conversion:** If your project uses Amplify Gen 1 (`amplifyconfiguration.json`), `AWSAppSyncConfiguration(with: .amplifyOutputs)` will **not** work with it directly — it requires the Gen 2 format (`amplify_outputs.json`). You can generate `amplify_outputs.json` from your existing Gen 1 backend using the `ampx` CLI: + +```bash +npx ampx generate outputs \ + --stack \ + --out-dir . \ + --outputs-version 1 +``` + +Find your stack name in `amplify/backend/amplify-meta.json` (under `providers.awscloudformation.StackName`) or in the AWS CloudFormation console. Alternatively, use the Amplify App ID: + +```bash +npx ampx generate outputs \ + --app-id \ + --branch \ + --out-dir . \ + --outputs-version 1 +``` + +The `--outputs-version 1` flag ensures the correct format is generated. After running this command, add the generated `amplify_outputs.json` to your Xcode project. + + + +### Without Amplify (or keeping Gen 1 config) + +If you prefer not to convert your config format (for example, if you want to keep using your existing Gen 1 `amplifyconfiguration.json` for Amplify Auth), you can configure the Apollo client manually: + +**For API Key auth:** + +```swift +let store = ApolloStore(cache: InMemoryNormalizedCache()) +let authorizer = APIKeyAuthorizer(apiKey: "") + +let interceptor = AppSyncInterceptor(authorizer) +let interceptorProvider = DefaultPrependInterceptorProvider( + interceptor: interceptor, + store: store) +let transport = RequestChainNetworkTransport( + interceptorProvider: interceptorProvider, + endpointURL: URL(string: "")!) +let apolloClient = ApolloClient( + networkTransport: transport, + store: store) +``` + +**For Cognito User Pool auth (using Amplify Auth for token fetching):** + +If you use Amplify for Cognito authentication but configure Apollo manually, you need to provide the endpoint URL from your AppSync configuration and use `AuthCognitoTokensProvider` to get the auth token: + +```swift +import AWSPluginsCore + +let store = ApolloStore(cache: InMemoryNormalizedCache()) + +// Get your endpoint URL from amplifyconfiguration.json or the AppSync console +let endpointURL = URL(string: "")! + +let authorizer = AuthTokenAuthorizer { + let session = try await Amplify.Auth.fetchAuthSession() + if let tokenProvider = session as? AuthCognitoTokensProvider { + let tokens = try tokenProvider.getCognitoTokens().get() + return tokens.accessToken + } + throw AuthError.unknown("Unable to get Cognito tokens") +} + +let interceptor = AppSyncInterceptor(authorizer) +let interceptorProvider = DefaultPrependInterceptorProvider( + interceptor: interceptor, + store: store) +let transport = RequestChainNetworkTransport( + interceptorProvider: interceptorProvider, + endpointURL: endpointURL) +let apolloClient = ApolloClient( + networkTransport: transport, + store: store) +``` + + + +You can find your AppSync endpoint URL in `amplifyconfiguration.json` under `api..endpoint`, or in the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) under your API's settings. + + + + + +**Which approach should you use?** If you are migrating a Gen 1 project and do not want to deal with config conversion, the manual endpoint approach is the simplest path — you keep `amplifyconfiguration.json` for Amplify Auth and point Apollo at your AppSync endpoint directly. If you prefer a single unified config, convert to `amplify_outputs.json` using the `ampx` CLI (see the Gen 2 Config section above). + + diff --git a/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/index.mdx b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/index.mdx new file mode 100644 index 00000000000..fc3b7f33b16 --- /dev/null +++ b/src/pages/gen1/[platform]/build-a-backend/more-features/datastore/migrate-from-datastore/set-up-apollo-kotlin/index.mdx @@ -0,0 +1,254 @@ +import { getCustomStaticPath } from '@/utils/getCustomStaticPath'; + +export const meta = { + title: 'Set up Apollo Kotlin', + description: 'Add Apollo Kotlin dependencies, configure the Gradle plugin, and set up the Apollo client with your AppSync endpoint and authentication.', + platforms: [ + 'android' + ] +}; + +export const getStaticPaths = async () => { + return getCustomStaticPath(meta.platforms); +}; + +export function getStaticProps(context) { + return { + props: { + platform: context.params.platform, + meta + } + }; +} + +Apollo Kotlin includes two main components: a Gradle plugin that reads your schema and operation files to generate type-safe Kotlin code, and a runtime client that executes requests using the generated code. This page covers adding dependencies, configuring the Gradle plugin, and setting up the Apollo client. + +## Add dependencies + +Add the following to your version catalog (`gradle/libs.versions.toml`): + +```toml +[versions] +apollo = "4.1.0" +apolloAppsync = "1.0.0" + +[libraries] +apollo-runtime = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" } +apollo-appsync = { group = "com.amplifyframework", name = "apollo-appsync", version.ref = "apolloAppsync" } +apollo-appsync-amplify = { group = "com.amplifyframework", name = "apollo-appsync-amplify", version.ref = "apolloAppsync" } + +[plugins] +apollo = { id = "com.apollographql.apollo", version.ref = "apollo" } +``` + + + +You need **two** AppSync Apollo Extensions dependencies: +- `com.amplifyframework:apollo-appsync` — provides core classes: `AppSyncEndpoint`, `AppSyncInterceptor`, and authorizer types (`ApiKeyAuthorizer`, `AuthTokenAuthorizer`, `IamAuthorizer`) +- `com.amplifyframework:apollo-appsync-amplify` — provides Amplify integration: `ApolloAmplifyConnector` for reading Amplify config and fetching auth tokens + +The base `apollo-appsync` library is **not** automatically exposed as a transitive dependency of `apollo-appsync-amplify`, so you must add both explicitly. + + + +Add the Apollo plugin to your root `build.gradle.kts`: + +```kotlin +plugins { + // ... existing plugins + alias(libs.plugins.apollo) apply false +} +``` + +Add the plugin and dependencies to your app `build.gradle.kts`: + +```kotlin +plugins { + // ... existing plugins + alias(libs.plugins.apollo) +} + +dependencies { + // Apollo Kotlin + implementation(libs.apollo.runtime) + + // AWS AppSync Apollo Extensions + implementation(libs.apollo.appsync) + implementation(libs.apollo.appsync.amplify) + + // Keep your existing Amplify Auth dependencies (if using Amplify for auth) + // implementation(libs.amplify.auth.cognito) + // implementation(libs.amplify.api) +} +``` + +## Configure the Apollo Gradle plugin + +Read the Apollo Kotlin [Getting Started section](https://www.apollographql.com/docs/kotlin#getting-started) for more information, and the [Gradle Plugin configuration](https://www.apollographql.com/docs/kotlin/advanced/plugin-configuration) documentation for all configuration options. + +Your Apollo configuration should look similar to this: + +```kotlin +apollo { + service("example") { + packageName.set("com.example.appsync") + schemaFile.set(file("src/main/graphql/schema.json")) + + // Map AppSync custom scalars to Kotlin String. + // Without this, fields like createdAt and updatedAt will be + // generated as `Any` instead of `String`. + // IMPORTANT: Only include scalars that are actually used in your schema. + // Apollo will error on unknown scalars. Check your downloaded schema.json + // to see which custom scalars your API uses. + mapScalarToKotlinString("AWSDateTime") + mapScalarToKotlinString("AWSTimestamp") + // Add any of the following only if your schema uses them: + // mapScalarToKotlinString("AWSDate") + // mapScalarToKotlinString("AWSTime") + // mapScalarToKotlinString("AWSEmail") + // mapScalarToKotlinString("AWSJSON") + // mapScalarToKotlinString("AWSURL") + // mapScalarToKotlinString("AWSPhone") + // mapScalarToKotlinString("AWSIPAddress") + } +} +``` + + + +**Custom scalars:** AWS AppSync uses custom GraphQL scalar types (such as `AWSDateTime`, `AWSJSON`). By default, Apollo generates these as `Any` in Kotlin, which means fields like `createdAt` and `updatedAt` will have type `Any` instead of `String`. Use `mapScalarToKotlinString()` to map them to `String`. Only map scalars that exist in your downloaded `schema.json` — Apollo 4.x will fail with an "unknown scalar" error if you map a scalar not present in the schema. A typical DataStore-backed AppSync schema uses `AWSDateTime` and `AWSTimestamp`. See the [AppSync Apollo Extensions type mapping docs](https://docs.amplify.aws/android/build-a-backend/data/aws-appsync-apollo-extensions/#type-mapping-appsync-scalars) for more details. + + + + + +**Scalar type generation:** In some Apollo versions, `mapScalarToKotlinString()` may not fully propagate to fragment fields — fields like `createdAt` and `updatedAt` may still be generated as `Any` in the `PostDetails` fragment class. At runtime, these values are strings, so you can safely use `.toString()` to work with them (for example, `post.createdAt?.toString()`). Check the generated code in `build/generated/source/apollo/` to verify the actual types. + + + + + +**Generated enum types:** Apollo Kotlin generates GraphQL enum types as **Kotlin sealed classes** rather than Java-style enums. Standard Java enum methods like `.values()` and `.valueOf()` will **not** work. Use `.knownEntries` instead: + +```kotlin +// This will NOT compile — Apollo enums are not Java enums +PostStatus.values() + +// Use this instead +PostStatus.knownEntries +``` + +If your UI code uses `.values()` to populate dropdowns or pickers, update all occurrences to `.knownEntries`. + + + +## Configure the Apollo client + +The runtime component of Apollo must be configured to connect to AWS AppSync, including handling authorization modes and the subscription protocol. The [AWS AppSync Apollo Extensions library](https://docs.amplify.aws/android/build-a-backend/data/aws-appsync-apollo-extensions/) implements the required logic. + +### With Amplify Gen 2 config (`amplify_outputs.json`) + +If your project uses Amplify Gen 2 (or you have converted your Gen 1 config — see the note below), you can use `ApolloAmplifyConnector` to read the endpoint and configure authorization automatically. + +**For API Key auth:** + +```kotlin +val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) +val apolloClient = ApolloClient.Builder() + .serverUrl(connector.endpoint.serverUrl.toString()) + .addHttpInterceptor(AppSyncInterceptor(connector.apiKeyAuthorizer())) + .build() +``` + +**For Cognito User Pool auth (owner-based authorization):** + +```kotlin +val connector = ApolloAmplifyConnector(context, AmplifyOutputs(R.raw.amplify_outputs)) +val apolloClient = ApolloClient.Builder() + .serverUrl(connector.endpoint.serverUrl.toString()) + .addHttpInterceptor(AppSyncInterceptor(connector.cognitoUserPoolAuthorizer())) + .build() +``` + + + +**Gen 1 to Gen 2 config conversion:** If your project uses Amplify Gen 1 (`amplifyconfiguration.json`), `ApolloAmplifyConnector` will **not** work with it directly — it requires the Gen 2 format (`amplify_outputs.json`). You can generate `amplify_outputs.json` from your existing Gen 1 backend using the `ampx` CLI: + +```bash +npx ampx generate outputs \ + --stack \ + --out-dir app/src/main/res/raw \ + --outputs-version 1 +``` + +Find your stack name in `amplify/backend/amplify-meta.json` or in the AWS CloudFormation console. Alternatively, use the Amplify App ID: + +```bash +npx ampx generate outputs \ + --app-id \ + --branch \ + --out-dir app/src/main/res/raw \ + --outputs-version 1 +``` + +The `--outputs-version` flag controls the output format: version `0` produces the classic `amplify-configuration` format, while version `1` produces the newer `amplify_outputs` format needed by `ApolloAmplifyConnector`. After running this command, place the generated `amplify_outputs.json` in `app/src/main/res/raw/`. + + + +### With Amplify Gen 1 config (`amplifyconfiguration.json`) — manual endpoint + +If you want to keep using your existing Gen 1 `amplifyconfiguration.json` for Amplify Auth without converting config formats, you can configure the Apollo client manually. You are still using Amplify (for authentication) — only the Apollo endpoint configuration is done manually. + +**For API Key auth:** + +```kotlin +val endpoint = AppSyncEndpoint("") +val authorizer = ApiKeyAuthorizer("") +val apolloClient = ApolloClient.Builder() + .serverUrl(endpoint.serverUrl.toString()) + .addHttpInterceptor(AppSyncInterceptor(authorizer)) + .build() +``` + +**For Cognito User Pool auth (using Amplify Auth for token fetching):** + +If you are still using Amplify for Cognito authentication but configuring Apollo manually, you can use `ApolloAmplifyConnector.fetchLatestCognitoAuthToken` to get the auth token. This method uses **callback-based** Java `Consumer` parameters, not Kotlin coroutines, so you need to wrap it with `suspendCoroutine`: + +```kotlin +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +val endpoint = AppSyncEndpoint("") +val authorizer = AuthTokenAuthorizer { + suspendCoroutine { cont -> + ApolloAmplifyConnector.fetchLatestCognitoAuthToken( + { token -> cont.resume(token) }, + { error -> cont.resumeWithException(error) } + ) + } +} + +val apolloClient = ApolloClient.Builder() + .serverUrl(endpoint.serverUrl.toString()) + .addHttpInterceptor(AppSyncInterceptor(authorizer)) + .build() +``` + + + +**Which approach should you use?** If you are migrating a Gen 1 project and do not want to deal with config conversion, the manual endpoint approach is the simplest path — you keep `amplifyconfiguration.json` for Amplify Auth and point Apollo at your AppSync endpoint directly. If you prefer a single unified config, convert to `amplify_outputs.json` using the `ampx` CLI (see the Gen 2 Config section above). + + + + + +**Finding your AppSync endpoint:** You can find your AppSync endpoint URL in the [AWS AppSync console](https://console.aws.amazon.com/appsync/home) under your API's **Settings** page, or in your `amplifyconfiguration.json` under `api > plugins > awsAPIPlugin > > endpoint`. + + + + + +**Import note:** `AppSyncEndpoint` and `AppSyncInterceptor` are in `com.amplifyframework.apollo.appsync`. `AuthTokenAuthorizer` and `ApiKeyAuthorizer` are in `com.amplifyframework.apollo.appsync.authorizers`. `ApolloAmplifyConnector` is in `com.amplifyframework.apollo.appsync` (provided by the `apollo-appsync-amplify` artifact). + +