Skip to content

Some cleanup to make the demo more compatible #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
Demo Django application which has HTTP endpoints to authorize a PowerSync enabled application to sync data between a client device and a Postgres database.
This repo compliments the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist)

## Note:
This demo backend uses ngrok to expose the JWKS endpoint to PowerSync Cloud. Ngrok has been progressively making their free version harder to use. Specifically, they now inject an interstitial warning page that requires a paid plan to remove. Therefore this demo app currently requires a paid ngrok plan. This demo will move to using the self-hosted PowerSync Open Edition in the future.

## Requirements

This app needs a Postgres instance that is hosted. For a free version for testing/demo purposes, visit [Supabase](https://supabase.com/).
Expand Down Expand Up @@ -39,10 +42,12 @@ python manage.py makemigrations
python manage.py migrate
```

Note that one of the migrations creates a test user in the `auth_user` table - you can use it to log into your frontend app. Take note of the user's id and update the hard coded id in the `upload_data` endpoint of `api/views.py` to match this user's id. In production you'd typically want to authenticate the user on this endpoint (using whatever auth mechanism you already have in place) before signing a JWT for use with PowerSync. See an example [here](https://github.com/powersync-ja/powersync-jwks-example/blob/151adf17611bef8a60d9e6cc490827adc4612da9/supabase/functions/powersync-auth/index.ts#L22)

6. Run the following SQL statement on your Postgres database:

```sql
create publication powersync for table api_list, api_todo;
create publication powersync for table lists, todos;
```

## Start App
Expand Down Expand Up @@ -86,7 +91,9 @@ Connections ttl opn rt1 rt5 p50 p90
1957 0 0.04 0.03 0.01 89.93
```

3. Open the PowerSync Dashboard and paste the `Forwarding` url starting with HTTPS into the Credentials tab of your PowerSync instance e.g.
3. Update ALLOWED_HOSTS in `todo_list_custom_backend/settings.py` to include your ngrok forwarding address e.g. `http://your_id.ngrok-free.app`, then restart your Django app.

4. Open the PowerSync Dashboard and paste the `Forwarding` url starting with HTTPS into the Credentials tab of your PowerSync instance e.g.

```
JWKS URI
Expand All @@ -95,4 +102,4 @@ https://your_id.ngrok-free.app/api/get_keys/

Pay special attention to the URL, it should include the `/api/get_keys/` path as this is used by the PowerSync server to validate tokens and the demo will not work without it.

4. Update the `AppConfig.ts` if you're using the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist) and set the `djangoUrl` value.
5. Update the `AppConfig.ts` if you're using the [PowerSync + Django React Native Demo: Todo List](https://github.com/powersync-ja/powersync-js/tree/main/demos/django-react-native-todolist) and set the `djangoUrl` value.
24 changes: 15 additions & 9 deletions api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.6 on 2023-10-10 17:13
# Generated by Django 4.2.6 on 2024-06-28 06:17

from django.db import migrations, models

Expand All @@ -12,25 +12,31 @@ class Migration(migrations.Migration):

operations = [
migrations.CreateModel(
name='List',
name='Lists',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('name', models.CharField(max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('name', models.CharField(max_length=100, null=True)),
('owner_id', models.CharField(max_length=36)),
],
options={
'db_table': 'lists',
},
),
migrations.CreateModel(
name='Todo',
name='Todos',
fields=[
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('description', models.TextField()),
('description', models.TextField(null=True)),
('completed', models.BooleanField(default=False)),
('created_by', models.CharField(max_length=36)),
('created_by', models.CharField(max_length=36, null=True)),
('completed_by', models.CharField(blank=True, max_length=36, null=True)),
('list_id', models.CharField(max_length=36)),
('list_id', models.CharField(max_length=36, null=True)),
],
options={
'db_table': 'todos',
},
),
]
18 changes: 0 additions & 18 deletions api/migrations/0002_alter_todo_description.py

This file was deleted.

19 changes: 19 additions & 0 deletions api/migrations/0002_create_test_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.db import migrations
from django.contrib.auth import get_user_model

def create_test_user(apps, schema_editor):
User = get_user_model()
User.objects.create_user(
username='testuser',
password='testpassword'
)

class Migration(migrations.Migration):

dependencies = [
('api', '0001_initial'),
]

operations = [
migrations.RunPython(create_test_user),
]
18 changes: 0 additions & 18 deletions api/migrations/0003_alter_todo_created_by.py

This file was deleted.

18 changes: 0 additions & 18 deletions api/migrations/0004_alter_todo_list_id.py

This file was deleted.

18 changes: 0 additions & 18 deletions api/migrations/0005_alter_list_created_at.py

This file was deleted.

23 changes: 0 additions & 23 deletions api/migrations/0006_alter_list_name_alter_todo_created_at.py

This file was deleted.

8 changes: 6 additions & 2 deletions api/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from django.db import models

class List(models.Model):
class Lists(models.Model):
id = models.CharField(max_length=36, primary_key=True) # Use CharField for 'id' as text
created_at = models.DateTimeField(auto_now_add=True, null=True) # Use DateTimeField for 'created_at'
name = models.CharField(max_length=100, null=True) # Use CharField for 'name' as text
owner_id = models.CharField(max_length=36) # Use CharField for 'owner_id' as text

class Meta:
db_table = 'lists' # Custom table name without app prefix
def __str__(self):
return self.name

class Todo(models.Model):
class Todos(models.Model):
id = models.CharField(max_length=36, primary_key=True) # Use CharField for 'id' as text
created_at = models.DateTimeField(auto_now_add=True, null=True) # Use DateTimeField for 'created_at'
completed_at = models.DateTimeField(null=True, blank=True) # Use DateTimeField for 'completed_at'
Expand All @@ -19,5 +21,7 @@ class Todo(models.Model):
completed_by = models.CharField(max_length=36, null=True, blank=True) # Use CharField for 'completed_by' as text
list_id = models.CharField(max_length=36, null=True)

class Meta:
db_table = 'todos' # Custom table name without app prefix
def __str__(self):
return self.description
2 changes: 1 addition & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from . import views

urlpatterns = [
path('get_token/', views.get_token, name='get_token'),
path('get_powersync_token/', views.get_powersync_token, name='get_powersync_token'),
path('get_keys/', views.get_keys, name='get_keys'),
path('get_session/', views.get_session, name='get_session'),
path('auth/', views.auth, name='auth'),
Expand Down
69 changes: 43 additions & 26 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
import api.app_utils as app_utils
from .models import Todo, List
from .models import Todos, Lists

@api_view(['GET'])
def get_token(request):
Expand All @@ -21,7 +21,6 @@ def get_token(request):
}, status=200)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

@api_view(['GET'])
def get_keys(request):
try:
Expand Down Expand Up @@ -52,20 +51,29 @@ def auth(request):
data = json.loads(request.body.decode('utf-8'))
username = data.get('username')
password = data.get('password')
user = User.objects.get(username=username, password=password)
if user is not None:
token = app_utils.create_jwt_token(user.id)
response = {'access_token': token}
return JsonResponse(response, status=200)
else:
return JsonResponse({'message': 'Authentication failed'}, status=401)
try:
user = authenticate(username=username, password=password)
if user is not None:
token = app_utils.create_jwt_token(user.id)
response = {'access_token': token}
return JsonResponse(response, status=200)
else:
logger.warning(f"Authentication failed for username: {username}")
return JsonResponse({'message': 'Authentication failed'}, status=401)

except json.JSONDecodeError as e:
logger.error(f"JSON decode error: {e}")
return JsonResponse({'message': 'Invalid JSON'}, status=400)
except Exception as e:
logger.error(f"Unexpected error: {e}")
return JsonResponse({'message': 'Internal server error'}, status=500)

@api_view(['PUT', 'PATCH', 'DELETE'])
def upload_data(request):
op = json.loads(request.body.decode('utf-8'))
data = op.get('data')
print(op)
if op.get('table') == 'api_todo':
if op.get('table') == 'todos':
if request.method == 'PUT':
upsertTodo(data)
return Response({'message': 'Todo updated'}, status=200)
Expand All @@ -74,12 +82,12 @@ def upload_data(request):
return HttpResponse({'message': 'Todo updated'}, status=200)
elif request.method == 'DELETE':
try:
todo = Todo.objects.get(id=data.get('id'))
todo = Todos.objects.get(id=data.get('id'))
todo.delete()
return HttpResponse({'message': 'Todo deleted'}, status=200)
except Todo.DoesNotExist:
except Todos.DoesNotExist:
return HttpResponse({'message': 'Todo does not exist'}, status=404)
elif op.get('table') == 'api_list':
elif op.get('table') == 'lists':
if request.method == 'PUT':
upsertList(data)
return Response({'message': 'List created'}, status=200)
Expand All @@ -88,45 +96,54 @@ def upload_data(request):
return HttpResponse({'message': 'List updated'}, status=200)
elif request.method == 'DELETE':
try:
list = List.objects.get(id=data.get('id'))
list = Lists.objects.get(id=data.get('id'))
list.delete()
return HttpResponse({'message': 'List deleted'}, status=200)
except List.DoesNotExist:
except Lists.DoesNotExist:
return HttpResponse({'message': 'List does not exist'}, status=404)

def upsertTodo(data):
try:
todo = Todo.objects.get(id=data.get('id'))
todo = Todos.objects.get(id=data.get('id'))
todo.description = data.get('description')
todo.created_by = data.get('created_by')
todo.list_id = data.get('list_id')
todo.save()
except Todo.DoesNotExist:
todo = Todo(id=data.get('id'), description=data.get('description'), created_by=data.get('created_by'), list_id=data.get('list_id'))
except Todos.DoesNotExist:
todo = Todos(id=data.get('id'), description=data.get('description'), created_by=data.get('created_by'), list_id=data.get('list_id'))
todo.save()

def updateTodo(data):
todo = Todo.objects.get(id=data.get('id'))
todo = Todos.objects.get(id=data.get('id'))
if todo is not None:
todo.description = data.get('description')
todo.created_by = data.get('created_by')
todo.list_id = data.get('list_id')
if 'description' in data:
todo.description = data.get('description')
if 'created_by' in data:
todo.created_by = data.get('created_by')
if 'list_id' in data:
todo.list_id = data.get('list_id')
if 'completed' in data:
todo.completed = data.get('completed')
if 'completed_by' in data:
todo.completed_by = data.get('completed_by')
if 'completed_at' in data:
todo.completed_at = data.get('completed_at')
todo.save()

def upsertList(data):
try:
list = List.objects.get(id=data.get('id'))
list = Lists.objects.get(id=data.get('id'))
list.created_at = data.get('created_at')
list.name = data.get('name')
list.owner_id = data.get('owner_id')
list.save()
return Response({'message': 'List updated'}, status=200)
except List.DoesNotExist:
list = List(id=data.get('id'), created_at=data.get('created_at'), name=data.get('name'), owner_id=data.get('owner_id'))
except Lists.DoesNotExist:
list = Lists(id=data.get('id'), created_at=data.get('created_at'), name=data.get('name'), owner_id=data.get('owner_id'))
list.save()

def updateList(data):
list = List.objects.get(id=data.get('id'))
list = Lists.objects.get(id=data.get('id'))
if list is not None:
list.created_at = data.get('created_at')
list.name = data.get('name')
Expand Down