Menu Docs

Tutorial: Migrar um aplicativo React Native para o PowerSync

A partir de 2024 de setembro, o Atlas Device SDKs (Realm), Device Sync e App Services foram descontinuados. Isso significa que os usuários desses serviços terão que migrar para outra solução até 2025 de setembro. Se precisar de mais tempo, entre em contato com o suporte.

O PowerSync é uma das principais alternativas ao Atlas Device Sync. É uma solução baseada em SQLite e pode ser a solução certa para migrar se você tiver um aplicação móvel usando o Device Sync.

Este tutorial guiará você pelas etapas necessárias para migrar um aplicação móvel do Device Sync , escrito no React Native, para o PowerSync. Seus dados de backend permanecerão no Atlas, então você precisará configurar o serviço PowerSync, atualizar os esquemas e vinculações do banco de dados local e configurar um serviço de backend para gravar no Atlas.

Este tutorial usa um aplicação de lista de tarefas do Realm para o React Native disponível no 2repositório de exemplo do powersync Realm.

Primeiro, você precisa implantar um Atlas Cluster e inserir alguns dados de teste. Isto irá orientá-lo como se estivesse configurando o Atlas pela primeira vez. Se você já tiver um cluster implantado, fique à vontade para pular.

  1. Navegue até MongoDB Atlas e registre-se para uma conta Atlas , ou entre se você já tiver uma conta.

  2. Em seguida, crie um cluster.

    Captura de tela da UI

    Para fins de teste, selecione o cluster M0 (grátis) com as configurações padrão. Sinta-se à vontade para fazer quaisquer alterações adicionais para atender às suas necessidades.

  3. Clique em Criar sistema.

    Captura de tela da UI

    Você retorna ao seu painel. O modal Conectar ao cluster é exibido automaticamente.

  4. Clique em Escolher um método de conexão e selecione Drivers.

    Captura de tela da UI

    Nesta tela, copie a URL que exibe na etapa 3.

    Captura de tela da UI

    Adicione a string de conexão ao código do seu aplicação . Esta é a sua string de conexão; é necessário acessar sua instância MongoDB . Salve a string de conexão para referência futura.

    Você criará um nome de usuário e uma senha nas próximas etapas que a instância do PowerSync usará para se conectar ao banco de dados.

  5. Clique em Concluído para fechar o modal.

    Após a conclusão da implantação do cluster, seu dashboard deverá ter a seguinte aparência.

  6. Clique em Adicionar dados para criar um novo banco de dados.

    Captura de tela da UI

    No cartão Criar um Banco de Dados no Atlas , clique em INICIAR.

    Captura de tela da UI

    Crie um banco de dados chamado PowerSync e uma collection chamada Item e clique em Criar banco de dados.

    Captura de tela da UI

    Você retorna ao painel e deve ver o banco de dados e a coleção recém-criados:

    Captura de tela da UI

    Finalmente, você precisa criar um novo usuário que o PowerSync usará para se conectar a esse banco de dados.

    Na barra lateral esquerda, clique em Acesso ao banco de dados no cabeçalho Segurança.

    Captura de tela da UI

    Clique em Add New Database User, crie um novo usuário chamado powersync e forneça uma senha. Observe o nome de usuário e a senha a serem usados na string de conexão que você copiou anteriormente.

    Observação

    Se o seu nome de usuário ou senha contiver qualquer um dos seguintes caracteres especiais, você deverá convertê-los em um formato seguro de URL para sua string de conexão: $, :, /, ?, !, #, [, ], @. Você pode fazer isso manualmente ou usar um aplicação de codificação de URL , como urtencoder.org.

    Na seção Privilégios do usuário do banco de dados, clique em Adicionar privilégio específico e adicione um privilégio para readWrite dbAdmin um role e um para o banco de dados PowerSync.

    Captura de tela da UI

    Clique em Adicionar Usuário.

    Você deve ver o usuário recém-criado com as permissões de banco de dados necessárias.

    Captura de tela da UI

Para obter mais detalhes sobre permissões de usuário, consulte a seção MongoDB do guia Configuração do banco de dados de origem PowerSync.

Para que o PowerSync acesse o banco de dados em execução no Atlas, é necessário adicionar os endereços IP do serviço à lista de acesso IP. Esses endereços IP estão listados na documentação Segurança da PowerSync e Filtragem de IP.

Na barra lateral esquerda, clique em Acesso à rede no cabeçalho Segurança.

Clique em + Adicionar Endereço IP e insira o endereço IP. Para melhor ajudar qualquer pessoa a administrar essa lista no futuro, também recomendamos inserir PowerSync como o comentário opcional.

Clique em Confirmar e repita para cada IP.

Atualize os espaços reservados na string de conexão que você copiou anteriormente com o nome de usuário e a senha do usuário de banco de dados.

Nesta etapa, você importará alguns dados de amostra que serão utilizados para sincronizar dados em etapas futuras.

Primeiro, instale o MongoDB Database Tools para obter acesso ao mongoimport. Consulte as instruções do Guia de instalação do seu sistema operacional.

Depois de instalar o database-tools, digite o seguinte no terminal para confirmar que você pode acessar o mongoimport:

mongoimport --version

Isso deve retornar a versão da ferramenta. Consulte o Guia de instalação acima se estiver tendo problemas.

Em seguida, crie um arquivo JSON denominado sample.json com o seguinte conteúdo:

[
{
"isComplete": false,
"summary": "Complete project documentation",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Buy groceries",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Schedule dentist appointment",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Prepare presentation for next week",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Pay utility bills",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Fix bug in login system",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Call mom",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Submit expense reports",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Plan team building event",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Review pull requests",
"owner_id": "mockUserId2"
}
]

Estes dados de amostra contêm alguns itens da lista de tarefas. O owner_id será usado para exemplos de filtragem mais tarde neste tutorial.

Para importar esse JSON, digite o seguinte comando, substituindo o espaço reservado <connection-string> por sua string de conexão:

mongoimport --uri="<connection-string>" --db=PowerSync --collection=Item
--file=sample.json --jsonArray

Você deve ver a seguinte mensagem:

10 document(s) imported successfully. 0 document(s) failed to import.

Caso contrário, confirme se seus parâmetros de comando (incluindo string de conexão) estão corretos e se seu usuário do Atlas tem o acesso correto ao banco de dados .

Você pode visualizar e gerenciar os documentos inseridos navegando até sua coleção na UI do Atlas ou usando o aplicação de desktop visual MongoDB Compass . Para exibir e gerenciar seu banco de dados e collections por meio do MongoDB Compass, você deve se conectar usando a mesma string de conexão.

Captura de tela da UI

Agora navegue até PowerSync e registre-se ou faça login.

Se você estiver fazendo login pela primeira vez, precisará criar uma nova instância para começar.

Crie uma nova instância denominada TodoList.

Captura de tela da UI

Selecione MongoDB como banco de dados de conexão .

Captura de tela da UI

Utilize sua string de conexão do Atlas para preencher as configurações de conexão.

Importante

Use uma versão reduzida da string de conexão que não contenha seu nome de usuário, senha ou outros parâmetros de URL . Por exemplo, sua conexão será parecida com mongodb+srv://m0cluster.h6folge.mongodb.net/.

Insira o nome do banco de dados ("PowerSync"), o nome de usuário ("Powersync") e a senha que você atribuiu a esta conta em uma etapa anterior.

Captura de tela da UI

Clique em Testar conexão para garantir que você possa se conectar com sucesso.

Se você vir o erro a seguir, confirme se todos os IPs de serviço PowerSync necessários estão em sua lista de acesso IP do Atlas .

Captura de tela da UI

Se você ainda estiver tendo problemas, consulte o Guia de conexão do banco de dados PowerSync para conexões MongoDB .

Clique em Avançar para implementar sua nova instância do PowerSync. Isso pode levar alguns minutos para ser concluído.

Depois que sua instância for implantada, você pode garantir que pode visualizar os dados migrados criando algumas regras básicas de sincronização.

Primeiro, remova as regras de sincronização padrão e substitua-as pelo seguinte:

bucket_definitions:
user_buckets:
parameters: SELECT request.user_id() as user_id
data:
- SELECT _id as id, * FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id

Para que os itens sejam sincronizados corretamente com o serviço PowerSync, observe o seguinte:

  • O _id deve ser mapeado para id.

  • O nome da collection "Item" deve ser colocado entre aspas. Isso ocorre porque o nome da nossa coleção começa com uma letra maiúscula.

  • Os blocos específicos do usuário devem corresponder a um user_id de global, que fornece acesso a todo o banco de dados. Caso contrário, você corresponderá ao user_id fornecido, que será recuperado do token de autenticação.

Observe que as regras do PowerSync Sync são um tópico bastante detalhado. Para saber mais, você pode conferir este blog post sobre Regras de Sincronização ou a documentação sobre as Regras de Sincronização do PowerSync.

Clique em Salvar e implantar. Mais uma vez, pode levar alguns minutos para que o sistema seja concluído.

Após a conclusão do sistema, você verá o seguinte:

Captura de tela da UI

Após a conclusão do sistema, você deverá ver o status apropriado.

Se você receber algum erro, verifique se o PowerSync usuário está configurado com as permissões listadas na documentação de configuração do banco de dados de origem do PowerSync.

Clique em Gerenciar instâncias para revisar as regras de sincronização e o status do sistema.

Para finalizar esta configuração, você usará o Aplicativo de Diagnóstico PowerSync para visualizar os itens da lista de tarefas que acabou de criar e adicionar às suas regras de sincronização. Para usar essa ferramenta, primeiro você precisa criar um token de desenvolvimento.

  • Na parte superior da página PowerSync, clique em Manage Instances.

  • Na barra lateral esquerda, clique nas reticências (...) ao lado de TodoList para abrir o menu de contexto desta instância,

e, em seguida, selecione Editar instância. - Selecione a aba Autenticação do cliente e clique em Habilitar tokens de desenvolvimento. - Clique em Salvar e implantar.

Captura de tela da UI

Clique nas reticências (...) ao lado de TodoList para abrir o menu de contexto dessa instância novamente e selecione Gerar token de desenvolvimento.

Você será solicitado a fornecer um assunto de token/user_id. Isso agirá como user_id e você pode configurar suas regras de sincronização para agir de acordo com ele.

Com as regras de sincronização que definimos anteriormente, você pode definir o Subject/User_id como global para gerar um token que terá acesso a todo o conjunto de dados. Você também pode definir isso como mockUserId ou mockUserId2 para sincronizar em um owner_idespecífico.

Copie o token gerado e, em seguida, abra o aplicativo de diagnóstico e cole o token de desenvolvimento.

Observação

O token de desenvolvimento expira em 12 horas. A ferramenta de diagnóstico interromperá a sincronização com o Atlas após a expiração, portanto, você deve gerar um novo token se quiser que ela retome a sincronização.

Você deve ver uma página semelhante a esta.

Captura de tela da UI

Na barra lateral esquerda, clique em Console SQL.

Crie uma query SELECT para visualizar todos os itens:

SELECT * FROM Item
Captura de tela da UI

Agora você tem todos os serviços necessários para sincronizar seu banco de dados MongoDB com um aplicação móvel.

Nesta fase, você vai clonar um aplicação de lista de tarefas do Realm para o React Native. A main ramificação do repositório de exemplo contém o resultado final da migração.

Para acompanhar este guia usando o repositório de exemplo , faça o checkout da ramificação 00-Start-Here:

git clone https://github.com/takameyer/realm2powersync
cd realm2powersync
git checkout 00-Start-Here

Em seguida, instale as dependências para que o editor possa selecionar quaisquer importações e garantir que não haja erros ao editar este projeto.

Importante

Este tutorial pressupõe que você tenha a versão mais recente do Node.js instalada.

npm install

Como o aplicação pressupõe que há um Atlas cluster com um serviço Device Sync ativo, ele ainda não pode ser executado. Nas próximas etapas, você fará as modificações necessárias para executar o projeto como um aplicação somente local.

Você deve remover as partes do Atlas Device Sync para que o aplicação seja executado com dados somente locais.

Primeiro, abra o source/AppWrapper.txs e remova a configuração do AppProvider, UserProvider e sync.

O arquivo AppWrapper.txs atualizado deve ser semelhante ao seguinte:

import React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import { RealmProvider } from '@realm/react';
import { App } from './App';
import { Item } from './ItemSchema';
const LoadingIndicator = () => {
return (
<View style={styles.activityContainer}>
<ActivityIndicator size="large" />
</View>
);
};
export const AppWrapper = () => {
return (
<RealmProvider schema={[Item]} fallback={LoadingIndicator}>
<App />
</RealmProvider>
);
};
const styles = StyleSheet.create({
activityContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-around',
padding: 10,
},
});

Em seguida, abra source/App.tsx e remova as partes sobre dataExplorerLink e os botões de cabeçalho para OfflineMode e Logout (isso será implementado posteriormente).

O arquivo App.tsx atualizado deve ser semelhante ao seguinte:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { LogoutButton } from './LogoutButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <LogoutButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
//headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

Por fim, abra o source/ItemListView.tsx e faça as seguintes atualizações:

  • Remova o código de assinatura Flexible Sync

  • Substituir usuário por um usuário simulado: - const user={ id: 'mockUserId' };

  • Remova quaisquer referências dataExplorerer

  • Remover funcionalidade da chave Show All Tasks (isso será implementado posteriormente)

O arquivo ItemListView.tsx atualizado deve ser semelhante ao seguinte:

import React, { useCallback, useState, useEffect } from 'react';
import { BSON } from 'realm';
import { useRealm, useQuery } from '@realm/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Alert, FlatList, StyleSheet, Switch, Text, View } from 'react-native';
import { Button, Overlay, ListItem } from '@rneui/base';
import { CreateToDoPrompt } from './CreateToDoPrompt';
import { Item } from './ItemSchema';
import { colors } from './Colors';
export function ItemListView() {
const realm = useRealm();
const items = useQuery(Item).sorted('_id');
const user = { id: 'mockUserId' };
const [showNewItemOverlay, setShowNewItemOverlay] = useState(false);
const [showAllItems, setShowAllItems] = useState(true);
// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
({ summary }: { summary: string }) => {
// if the realm exists, create an Item
realm.write(() => {
return new Item(realm, {
summary,
owner_id: user?.id,
});
});
},
[realm, user],
);
// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and delete it
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't delete someone else's task!");
} else {
realm.write(() => {
realm.delete(item);
});
}
}
},
[realm, user],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and update it's 'isCompleted' field
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't modify someone else's task!");
} else {
realm.write(() => {
item.isComplete = !item.isComplete;
});
}
}
},
[realm, user],
);
return (
<SafeAreaProvider>
<View style={styles.viewWrapper}>
<View style={styles.toggleRow}>
<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{ true: '#00ED64' }}
onValueChange={() => {
setShowAllItems(!showAllItems);
}}
value={showAllItems}
/>
</View>
<Overlay
isVisible={showNewItemOverlay}
overlayStyle={styles.overlay}
onBackdropPress={() => setShowNewItemOverlay(false)}>
<CreateToDoPrompt
onSubmit={({ summary }) => {
setShowNewItemOverlay(false);
createItem({ summary });
}}
/>
</Overlay>
<FlatList
keyExtractor={item => item._id.toString()}
data={items}
renderItem={({ item }) => (
<ListItem key={`${item._id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
{!item.isComplete && (
<Button
title="Mark done"
type="clear"
onPress={() => toggleItemIsComplete(item._id)}
/>
)}
<Button
title="Delete"
type="clear"
onPress={() => deleteItem(item._id)}
/>
</ListItem.Content>
</ListItem>
)}
/>
<Button
title="Add To-Do"
buttonStyle={styles.addToDoButton}
onPress={() => setShowNewItemOverlay(true)}
/>
</View>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
viewWrapper: {
flex: 1,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
addToDoButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
completeButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
showCompletedButton: {
borderRadius: 4,
margin: 5,
},
showCompletedIcon: {
marginRight: 5,
},
itemTitle: {
flex: 1,
},
itemSubtitle: {
color: '#979797',
flex: 1,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
},
toggleText: {
flex: 1,
fontSize: 16,
},
overlay: {
backgroundColor: 'white',
},
status: {
width: 40,
height: 40,
justifyContent: 'center',
borderRadius: 5,
borderWidth: 1,
borderColor: '#d3d3d3',
backgroundColor: '#ffffff',
alignSelf: 'flex-end',
},
delete: {
alignSelf: 'flex-end',
width: 65,
marginHorizontal: 12,
},
statusCompleted: {
borderColor: colors.purple,
},
statusIcon: {
textAlign: 'center',
fontSize: 17,
color: colors.purple,
},
});

Com essas alterações, o aplicativo deve funcionar em um banco de dados local.

Antes de iniciar a migração, você precisa construir e executar o aplicação atualizado para verificar se ele funciona conforme o esperado.

Para iOS, execute os seguintes comandos:

npx pod-install
npm run ios

Para Android, execute o seguinte comando:

npm run android

Observe que quaisquer erros de construção estão fora do escopo desta documentação. Se você estiver enfrentando problemas relacionados à compilação, consulte a documentação do React Native para garantir que seu ambiente esteja configurado corretamente.

Enquanto seu aplicativo está em execução, você pode verificar a funcionalidade básica. Você deve ser capaz de:

  • Criar novos itens

  • Marcar itens como concluídos

  • Excluir itens

Captura de tela da UI

Agora que você tem um aplicação Realm somente local em execução, pode começar a converter esse aplicação para usar uma versão somente local do cliente PowerSync.

O PowerSync usa um banco de dados baseado em SQLite, portanto, será necessário fazer algumas modificações no esquema para garantir que seja compatível.

Para fazer isso, você precisará configurar o cliente PowerSync. Para obter instruções detalhadas, você pode consultar o repositório npm @PowerSync/react-native ou a documentação do PowerSync React Native Setup.

Primeiro, execute o seguinte comando para adicionar dependências para o PowerSync React Native Client, o banco de dados SQLite de apoio, um iterador assíncrono polyfill (necessário conforme as instruções), bem como o bson dependência (usada para gerar ObjectId's para inserir documentos no MongoDB):

npm install @powersync/react-native @journeyapps/react-native-quick-sqlite @azure/core-asynciterator-polyfill bson

Para configurar o polyfill, abra o index.js e adicione import '@azure/core-asynciterator-polyfill'; ao topo do arquivo.

O arquivo index.js atualizado deve ser semelhante ao seguinte:

import '@azure/core-asynciterator-polyfill';
import 'react-native-get-random-values';
import {AppRegistry} from 'react-native';
import {AppWrapper} from './source/AppWrapper';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => AppWrapper);

Agora que as dependências foram adicionadas, você precisa reconstruir o aplicação:

  • Para iOS, execute pod-install.

  • Para Android, atualize o SDK mínimo exigido para 24 para ser compatível com react-native-quick-sqlite. Para fazer isso, abra android/build.gradle e altere minSdkVersion de 21 para 24.

Agora você configurará os tipos de dados e esquemas para o banco de dados local.

Consulte a documentação de mapeamento de tipo do PowerSync MongoDB para determinar como configurar seu esquema específico. A seguir, uma referência rápida dos tipos disponíveis:

Tipo
Descrição

zero

valores indefinidos ou valores não definidos

inteiro

um número inteiro assinado de 64bits

real

um número de ponto flutuante de 64bits

text

uma string de texto UTF-8

blob

Dados binários

Para este tutorial, você modificará source/ItemSchema.tsx da seguinte forma:

import {column, Schema, Table} from '@powersync/react-native';
export const ItemSchema = new Table({
isComplete: column.integer,
summary: column.text,
owner_id: column.text,
});
export const AppSchema = new Schema({
Item: ItemSchema,
});
export type Database = (typeof AppSchema)['types'];
export type Item = Database['Item'];

Importante

O nome da propriedade passado para Schema representa o nome da tabela local e da collection MongoDB . Neste caso, certifique-se de que ele seja nomeado Item.

Observe que este código exporta os tipos diretamente do AppSchema, em vez de defini-los manualmente.

Para obter acesso ao PowerSync e vincular seus dados, você precisará de acesso aos hooks e provedores do cliente PowerSync. Esta funcionalidade é fornecida pelo componente PowerSyncContext.

Primeiro, atualize o source/AppWrapper.tsx para usar o PowerSyncContext e inicialize seu cliente PowerSync:

import React from 'react';
import {App} from './App';
import {AppSchema} from './ItemSchema';
import {PowerSyncContext, PowerSyncDatabase} from '@powersync/react-native';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

Em seguida, atualize ItemListView.tsx para usar o cliente PowerSync. Para conseguir isso, você deve atualizar os ganchos usados na parte superior deste componente:

  • Para obter acesso ao banco de dados local para fazer gravações e atualizações, use o hook usePowerSync.

  • Para obter uma lista de itens da lista de tarefas que são renderizados automaticamente na atualização, use o hook useQuery.

Faça as seguintes alterações:

  • Remover import { BSON } from 'realm';

  • Adicionar import { ObjectId } from 'bson';

  • Altere as duas primeiras linhas da função ItemListView para corresponder ao seguinte:

    export function ItemListView() {
    const db = usePowerSync();
    const {data: items} = useQuery<Item>('SELECT * FROM Item');

Em seguida, você precisa atualizar os métodos createItem, deleteItem e toggleItemIsComplete.

Para cada um desses métodos, você usará o objeto db retornado de usePowerSync. Assim como no Realm, o banco de dados local abre uma transação para executar qualquer operação mutável, como inserir, atualizar ou excluir. Você também adicionará blocos try/catch para propagar quaisquer erros no frontend do aplicação.

Observe que o código está importando ObjectId de bson para criar os IDs exclusivos para cada item. Lembre-se de que o PowerSync espera que os itens da chave primária sejam denominados id.

O código de criação também implementa os valores padrão para os itens diretamente nessa lógica. Nesse caso, isComplete é inicializado como falso e o id é inicializado com o resultado de string do ObjectId recém-criado.

O método createItem pode ser implementado da seguinte forma:

// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
async ({summary}: {summary: string}) => {
try {
// start a write transaction to insert the new Item
db.writeTransaction(async tx => {
await tx.execute(
'INSERT INTO Item (id, summary, owner_id, isComplete) VALUES (?, ?, ?, ?)',
[new ObjectId().toHexString(), summary, user?.id, false],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

Os métodos deleteItem e toggleItemIsComplete são semelhantes, portanto, implemente-os da seguinte maneira:

// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
async (id: String) => {
// start a write transaction to delete the Item
try {
db.writeTransaction(async tx => {
await tx.execute('DELETE FROM Item WHERE id = ?', [id]);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
async (id: String) => {
// start a write transaction to update the Item
try {
db.writeTransaction(async tx => {
await tx.execute(
'UPDATE Item SET isComplete = NOT isComplete WHERE id = ?',
[id],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

Finalmente, atualize o FlatList renderizado. Você irá:

  • Substituir instâncias de _id por id

  • Atualize o keyExtractor do FlatList para usar a string id diretamente.

  • Anteriormente, o banco de dados retornava um ObjectId. Isso precisará ser convertido em uma string.

O FlatList atualizado agora se parece com o seguinte:

<FlatList
keyExtractor={item => item.id}
data={items}
renderItem={({item}) => (
<ListItem key={`${item.id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
<Pressable
accessibilityLabel={`Mark task as ${
item.isComplete ? 'not done' : 'done'
}`}
onPress={() => toggleItemIsComplete(item.id)}
style={[
styles.status,
item.isComplete && styles.statusCompleted,
]}>
<Text style={styles.statusIcon}>
{item.isComplete ? '✓' : '○'}
</Text>
</Pressable>
</ListItem.Content>
<ListItem.Content>
<Pressable
accessibilityLabel={'Remove Item'}
onPress={() => deleteItem(item.id)}
style={styles.delete}>
<Text style={[styles.statusIcon, {color: 'blue'}]}>
DELETE
</Text>
</Pressable>
</ListItem.Content>
</ListItem>
)}
/>

Depois de atualizar o código, você poderá usar um cliente PowerSync local.

Para verificar, reconstrua o aplicação. Se você estiver usando o iOS, não se lembre de atualizar os Pods com npx pod-install.

Captura de tela da UI

Agora você deve ter um aplicação funcional que permita adicionar, atualizar e excluir itens da lista de tarefas usando o PowerSync.

Se você encontrar problemas, poderá visualizar as alterações feitas até esse ponto na ramificação 02-Migrate-Local-Client do repositório de exemplo .

Seu aplicação móvel agora está pronto para sincronizar dados em tempo real do MongoDB.

Observação

Você provavelmente notou que os dados do Realm ainda não foram migrados. Este guia pressupõe que o cluster MongoDB hospedado no Atlas é a fonte da verdade para os dados e sincronize isso com o aplicação. A migração de dados locais está fora do escopo deste tutorial, mas pode ser abordada em documentação futura.

Agora você deve ter um serviço PowerSync em execução que contenha dados sincronizados do Atlas, que foram verificados usando a ferramenta de diagnóstico PowerSync.

Nesta fase, você obterá esses dados para sincronizar com o aplicação React Native .

Para começar, você precisa criar uma maneira de definir algumas variáveis de ambiente para tokens e endpoints.

Primeiro, instale react-native-dotenv em suas dependências de desenvolvimento. Este é um plugin-in babel que pega um arquivo .env da raiz do projeto e permite importar variáveis de ambiente diretamente para o aplicação.

npm install -D react-native-dotenv

Em seguida, adicione a seguinte linha ao arquivo babel.config.js:

module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['module:react-native-dotenv'],
};

Crie um novo diretório denominado types e, nele, crie um novo arquivo denominado env.d.ts que contenha as seguintes variáveis que queremos importar:

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
}

Você precisa recuperar os valores necessários para as variáveis de ambiente do PowerSync.

  • No console do PowerSync, na barra lateral esquerda, clique em ... ao lado de TodoList para abrir o menu de contexto.

  • Selecione Editar instância.

  • Copie e salve o URL.

Captura de tela da UI

Em seguida, gere um novo token de desenvolvimento para sua instância com o assunto/user_id. mockUserId Copie e salve o token gerado.

No projeto do aplicação , crie um arquivo .env no diretório raiz e cole o endpoint e o token do PowerSync que você acabou de gerar:

POWERSYNC_ENDPOINT=<endpoint>
AUTH_TOKEN=<dev-token>

Você precisará refatorar um pouco seu aplicação para que ele possa se conectar à sua instância do PowerSync.

Primeiro, crie um novo arquivo em source chamado PowerSync.ts e cole o seguinte:

import { AppSchema } from './ItemSchema';
import {
AbstractPowerSyncDatabase,
PowerSyncDatabase,
} from '@powersync/react-native';
import { AUTH_TOKEN, POWERSYNC_ENDPOINT } from '@env';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
class Connector {
async fetchCredentials() {
return {
endpoint: POWERSYNC_ENDPOINT,
token: AUTH_TOKEN,
};
}
async uploadData(database: AbstractPowerSyncDatabase) {
console.log('Uploading data');
}
}
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector);
return powerSync;
};
export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};

Este arquivo faz o seguinte:

  • Cria uma nova classe Connector , que será usada para definir o token de desenvolvimento e o endpoint PowerSync em nosso cliente PowerSync.

  • Define uma função simulada uploadData, que será utilizada na fase seguinte para enviar alterações para o Atlas.

  • Define métodos para configurar e redefinir nosso cliente PowerSync. A redefinição do cliente será útil para o desenvolvimento agora, pois todas as alterações feitas serão colocadas em uma fila. Até que essas alterações sejam processadas, você não receberá novas atualizações.

Em seguida, atualize AppWrapper.tsx para usar o novo método setupPowerSync:

import { PowerSyncContext } from '@powersync/react-native';
import React from 'react';
import { App } from './App';
import { setupPowerSync } from './PowerSync';
const powerSync = setupPowerSync();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

Em seguida, refatore LogoutButton.tsx para implementar o método resetPowerSync. Renomeie-o para ResetButton.tsx e atualize seu conteúdo da seguinte forma:

import React, { useCallback } from 'react';
import { Pressable, Alert, View, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import { resetPowerSync } from './PowerSync';
export function ResetButton() {
const signOut = useCallback(() => {
resetPowerSync();
}, []);
return (
<Pressable
onPress={() => {
Alert.alert('Reset Database?', '', [
{
text: 'Yes, Reset Database',
style: 'destructive',
onPress: () => signOut(),
},
{ text: 'Cancel', style: 'cancel' },
]);
}}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>Reset</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonContainer: {
paddingHorizontal: 12,
},
buttonText: {
fontSize: 16,
color: colors.primary,
},
});

Em seguida, modifique App.tsx para mostrar o botão Reset no lado esquerdo do cabeçalho:

  • Substitua import { LogoutButton } from './LogoutButton'; por import { ResetButton } from './ResetButton';

  • No headerLeft, substitua a linha existente por return <ResetButton />;

  • Descomente a linha //headerLeft para que o botão Redefinir seja exibido.

Suas alterações serão parecidas com o seguinte:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ResetButton } from './ResetButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <ResetButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

Por último, a biblioteca react-native-dotenv exige que nosso servidor React Native seja redefinido com um cache limpo, o que é normal ao adicionar funcionalidade ao Babel.

Para fazer isso, elimine qualquer instância do React Native atualmente em execução com ctrl-c e, em seguida, insira o seguinte para executar a instância com um cache limpo:

npm start -- --reset-cache

Agora você deve estar pronto para sincronizar seus dados do Atlas com seu aplicação React Native .

Agora redefina o aplicação. Se você fez modificações no banco de dados local do aplicativo antes, precisará clicar no novo botão Reset para redefinir o banco de dados local com o conteúdo do que está armazenado no Atlas.

Você deve agora ver todos os itens da lista de tarefas de mockUserId:

Captura de tela da UI

Se você encontrar problemas, exclua o aplicação em seu emulador/simulador e reconstrua-o para começar do zero.

Se você ainda estiver encontrando problemas, pode visualizar as alterações feitas até esse ponto na ramificação 03-Sync-Data-From-Atlas do repositório de exemplo .

Agora que seus dados estão sincronizando com o aplicação móvel, a próxima etapa é criar uma maneira de propagar as alterações locais no Atlas.

Nesta fase, você:

  • Implemente o método uploadData em seu Connector

  • Criar um servidor de backend simples para lidar com operações do dispositivo móvel

Por uma questão de simplicidade, este guia executará o servidor localmente. Para casos de uso de produção, considere o uso de um serviço de nuvem para lidar com essas solicitações (por exemplo O JournalApps oferece funções de nuvem sem servidor para ajudar com isso).

Comece examinando as operações enviadas para o método uploadData quando são feitas alterações locais no aplicação móvel.

Faça as seguintes alterações em source/PowerSync.ts:

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
console.log('batch', JSON.stringify(batch, null, 2));
}

Em seguida, você fará alterações no aplicação móvel que incluem:

  • Excluindo um item

  • Alternar um item como completo ou incompleto

  • Adicionando um novo item

Conclua a implementação do método uploadData para enviar essas informações em uma solicitação de busca.

Primeiro, adicione um novo valor ao seu .env:

BACKEND_ENDPOINT=http://localhost:8000

e types/env.d.ts:

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
export const BACKEND_ENDPOINT: string;
}

Se você estiver usando o emulador Android, deverá garantir que as solicitações para localhost na porta 8000 estejam sendo encaminhadas do emulador para sua máquina local. Para habilitar isso, execute o seguinte comando:

adb reverse tcp:8000 tcp:8000

Em seguida, adicione BACKEND_ENDPOINT à declaração de importação em source/PowerSync.ts:

import { AUTH_TOKEN, POWERSYNC_ENDPOINT, BACKEND_ENDPOINT } from '@env';

Em seguida, atualize o método uploadData:

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
if (batch === null) {
return;
}
const result = await fetch(`${BACKEND_ENDPOINT}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(batch.crud),
});
if (!result.ok) {
throw new Error('Failed to upload data');
}
batch.complete();
}

O método atualizado agora enviará uma matriz de operações CRUD para o endpoint de backend:

  • Se o aplicação estiver offline, ele simplesmente falhará.

  • Se o aplicação receber uma resposta positiva, ele marcará as operações como concluídas e o lote de operações será removido do aplicação móvel.

Agora, crie uma nova pasta no seu projeto chamada backend:

mkdir backend

Em seguida, crie um arquivo package.json:

{
"main": "index.js",
"scripts": {
"start": "node --env-file=.env index.js"
},
"dependencies": {
"express": "^4.21.2",
"mongodb": "^6.12.0"
}
}

Este package.json inclui um script start que adiciona variáveis de um .env ao serviço.

Crie um novo .env com seu Atlas connection string de antes:

MONGODB_URI=<connection_string>

Agora, instale as dependências:

npm install

Observe que este guia não incluirá como adicionar Typescript e outras ferramentas a este serviço, mas você pode fazê-lo à vontade. Além disso, o guia mantém a validação no mínimo e implementa apenas as alterações necessárias para preparar os dados recebidos do aplicação móvel para serem inseridos no MongoDB.

Primeiro, crie um index.js com o seguinte conteúdo:

const express = require("express");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
app.use(express.json());
// MongoDB setup
const client = new MongoClient(
process.env.MONGODB_URI || "mongodb://localhost:27017",
);
// Helper function to coerce isComplete to boolean
function coerceItemData(data) {
if (data && "isComplete" in data) {
data.isComplete = !!Number(data.isComplete);
}
return data;
}
async function start() {
await client.connect();
const db = client.db("PowerSync");
const items = db.collection("Item");
app.post("/update", async (req, res) => {
const operations = req.body;
try {
for (const op of operations) {
console.log(JSON.stringify(op, null, 2));
switch (op.op) {
case "PUT":
await items.insertOne({
...coerceItemData(op.data),
_id: new ObjectId(op.id),
});
break;
case "PATCH":
await items.updateOne(
{ _id: new ObjectId(op.id) },
{ $set: coerceItemData(op.data) },
);
break;
case "DELETE":
await items.deleteOne({
_id: new ObjectId(op.id),
});
break;
}
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(8000, () => {
console.log("Server running on port 8000");
});
}
start().catch(console.error);

Observe a partir do serviço acima que o isComplete é forçado em um valor boolean. Isso garante que os novos itens do todolist cheguem ao MongoDB com true ou false em vez de 1 ou 0. Uma instância ObjectId também está sendo criada a partir do op.id. Definir isso para a propriedade _id moldará os dados de acordo com os requisitos e as melhores práticas do MongoDB .

Agora você pode ativar o servidor:

npm start

O aplicação móvel já deve estar tentando enviar operações para esse endpoint. A declaração console.log deve mostrar as solicitações conforme elas estão sendo enviadas e as alterações devem se propagar para o Atlas.

Você pode verificar isso visualizando sua coleção do MongoDB na UI do Atlas ou no MongoDB Compass.

Captura de tela da UI

Agora você deve ter um aplicação móvel totalmente funcional que sincroniza dados de e para o Atlas. Você também pode tentar desligar o Wi-Fi para testar como o aplicativo funciona quando está offline.

Se você encontrar problemas, poderá visualizar as alterações feitas até esse ponto na ramificação 04-Write-To-Backend do repositório de exemplo .

Esta fase final aborda como implementar duas funcionalidades opcionais de teste de aplicação, bem como como limpar seu projeto de qualquer código e dependências desnecessários.

No processo de criação deste aplicação, as seguintes funcionalidades foram omitidas: Mostrar Todas as Tarefas e a chave de modo Offline. Esses recursos são úteis para testar a funcionalidade do aplicativo e não se destinam a ser usados em um aplicação de produção.

Observação

As etapas relacionadas a esses recursos são marcadas como opcionais. Sinta-se livre para pular estas etapas opcionais se isso não for do seu interesse.

Para implementar a alternância opcional Mostrar todos, será criado um segundo bucket que será ativado com base em um parâmetro do cliente . Você aplicará isso desconectando a sessão de sincronização atual e reconectando com um novo conjunto de valores. Esse valor será um booleano chamado view_all, que será usado como um backdoor inseguro para mostrar todos os itens da lista de tarefas já criados no cluster. Essa funcionalidade ajuda a mostrar que os buckets podem ser criados dinamicamente com base em determinados parâmetros.

Observação

A metodologia usada aqui é insegura, portanto, o sinalizador accept_potentially_dangerous_queries precisará ser ativado no bucket para que isso seja feito. Uma maneira segura de fazer isso seria baseá-lo em uma função de usuário e atualizar as autorizações dos usuários em seu banco de dados de apoio, que está fora do escopo deste guia.

Para começar, navegue até o painel do PowerSync e atualize as regras de sincronização para incluir um bucket com base no parâmetro view_all que está sendo definido:

bucket_definitions:
user_buckets:
parameters:
- SELECT request.user_id() as user_id
data:
- SELECT _id as id FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id
view_all_bucket:
accept_potentially_dangerous_queries: true
parameters:
- SELECT (request.parameters() ->> 'view_all') as view_all
data:
- SELECT _id as id FROM "Item" WHERE bucket.view_all = true

Observe que as definições de bucket são combinadas juntas, portanto, quando o view_all_bucket estiver ativo, ele será adicionado aos dados do user_buckets.

Em seguida, atualize source/PowerSync.ts em seu projeto para incluir uma variável local para determinar o estado de sinalizador view_all e aplique-o aos parâmetros da instância de conexão.

Primeiro, adicione um parâmetro viewAll e atualize a função setupPowerSync:

let viewAll = false;
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector, {params: {view_all: viewAll}});
return powerSync;
};

Em seguida, adicione as seguintes duas funções:

export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};
export const toggleViewAll = () => {
viewAll = !viewAll;
resetPowerSync();
};

Finalmente, atualize source/ItemListView.tsx.

Primeiro, importe toggleViewAll de PowerSync:

import { toggleViewAll } from './PowerSync';

Em seguida, modifique o atributo onValueChange da opção "Mostrar todas as tarefas" para invocar o método toggleViewAll. Use o seguinte código para substituir os componentes Text e Switch:

<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{true: '#00ED64'}}
onValueChange={() => {
setShowAllItems(!showAllItems);
toggleViewAll();
}}
value={showAllItems}
/>

Agora reinicie seu aplicação e verifique se ele funciona conforme o esperado:

Captura de tela da UI

Para implementar a alternância opcional do Modo offline, você precisará desconectar a sessão de sincronização e reconectá-la. Isso permitirá que você faça alterações locais enquanto não estiver conectado à sincronização e verifique se elas são enviadas quando a sessão de sincronização for restabelecida.

Você adicionará uma variável para o estado da conexão e, em seguida, criará um método para alternar isso e invocar os métodos connect e disconnect no cliente PowerSync.

Primeiro, adicione o seguinte a source/PowerSync.ts:

let connection = true;
export const toggleConnection = () => {
if (connection) {
powerSync.disconnect();
} else {
setupPowerSync();
}
connection = !connection;
};

Em seguida, refatore o source/OfflineModeButton.tsx para remover a funcionalidade Realm e substitua-a invocando o novo método toggleConnection. Você também precisará adicionar algumas importações:

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import {toggleConnection} from './PowerSync';
export function OfflineModeButton() {
const [pauseSync, togglePauseSync] = useState(false);
return (
<Pressable
onPress={() => {
toggleConnection();
togglePauseSync(!pauseSync);
}}>
<Text style={styles.buttonText}>
{pauseSync ? 'Enable Sync' : 'Disable Sync'}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonText: {
padding: 12,
color: colors.primary,
},
});

Finalmente, abra o source/App.tsx e descomente o componente headerRight de volta no Stack.Screen do aplicação:

<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
headerRight,
}}
/>

Agora, verifique as atualizações abrindo uma segunda instância do aplicativo e, em seguida, fazendo algumas alterações:

Captura de tela da UI

Finalmente, você pode limpar seu projeto.

Os seguintes arquivos podem ser excluídos com segurança:

  • atlasConfig.json

  • source/WelcomeView.tsx

Você também pode remover as seguintes dependências do seu package.json:

  • @realm/react

  • realm

Este guia deve ter fornecido a você os blocos de construção para iniciar sua viagem de migração para o PowerSync.

Para resumir, seguindo este guia, você deverá ter realizado o seguinte:

  • Implementou um banco de dados MongoDB com dados de amostra

  • Implementou um serviço PowerSync que sincroniza os dados de amostra

  • Aprendou como visualizar e consultar esses dados usando a Ferramenta de Diagnóstico

  • Converteu um aplicação móvel do Device Sync apenas para local

  • Migrado de um banco de dados Realm somente local para o PowerSync

  • Configurar a sincronização do PowerSync com um banco de dados móvel

  • Criou um backend para enviar alterações do cliente PowerSync para MongoDB

Para as próximas etapas, tente pegar um pequeno fragmento do seu aplicação móvel e convertê-lo para usar o PowerSync. E fique atento à documentação futura que aborda casos de uso mais avançados.