Skip to content
This repository was archived by the owner on Sep 19, 2024. It is now read-only.
Draft
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
264 changes: 92 additions & 172 deletions seed/supabase-twitter-clone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Setup OAuth for Local Development](#setup-oauth-for-local-development)
- [Setup an Email+Password Login for Local Development](#setup-an-emailpassword-login-for-local-development)
- [Setup @snaplet/seed](#setup-snapletseed)
- [Writing the Seed Script](#writing-the-seed-script)
- [Snaplet Seed with E2E](#snaplet-seed-with-e2e)
- [Conclusion](#conclusion)
- [Acknowledgments](#acknowledgments)
Expand Down Expand Up @@ -60,7 +61,6 @@ First, let's set up a local development environment for the Supabase Twitter clo

```bash
npx supabase login
npx supabase init
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this when I ran this:

 failed to create config file: open supabase/config.toml: file exists
Try rerunning the command with --debug to troubleshoot the error.

I guess we don't need to init anymore since we already checked in the stuff that was generated during init.

```

![supabase-init-asciinema](https://github.com/snaplet/examples/assets/8771783/10f11bca-5dd5-42ac-b81a-b33d6016026e)
Expand All @@ -71,23 +71,11 @@ First, let's set up a local development environment for the Supabase Twitter clo
# Your projectID can be found using the `supabase projects list` command and noting the REFERENCE ID value.
# Input your remote database password when prompted.
npx supabase link --project-ref <your-twitter-clone-project-id>
# Create a valid migrations folder for Supabase to pull the first migration.
mkdir -p supabase/migrations
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, the dir already existed. I guess we could keep this around regardless (won't error or anything), but didn't seem needed anymore.


# Pull the database schema from the remote project.
npx supabase db pull
```

This process creates a new `remote_schema.sql` file within the `supabase/migrations` folder. However, this migration lacks the necessary triggers and publications for our real-time updates to function correctly. Thus, we need to manually add them to the `remote_schema.sql` file:

```sql
-- Append at the end
-- Trigger to create a profile for a user upon creation
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION "public"."create_profile_for_user"();
-- Publication for the tweets table to enable real-time functionality
ALTER PUBLICATION "supabase_realtime" ADD TABLE "public"."tweets";
RESET ALL;
```
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were already in the migration file that was checked into the repo, so I guessed we don't need these lines in the readme anymore: https://github.com/snaplet/examples/blob/main/seed/supabase-twitter-clone/supabase/migrations/20240312180754_remote_schema.sql#L169-L171


Next, we must synchronize our local development project with the remote one:

```bash
Expand Down Expand Up @@ -183,44 +171,6 @@ npm run dev

Although OAuth login works, it's not the most efficient method for automating testing or quickly logging into different personas, as it would require multiple GitHub accounts. Let's address this issue next.

### Setup an Email+Password Login for Local Development

For local development and testing, it's crucial to have the ability to log in as different personas easily. This can be achieved by creating a new user with pre-filled data. We can facilitate this by setting up an email and password login mechanism, and then utilize the Supabase admin interface to add specific data to it.

Firstly, we'll create a utility route for development purposes. This route will allow us to easily log in as a user using an email and password. To accomplish this, create a new route at `app/auth/dev/login/route.ts` with the following content:

```ts
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse, type NextRequest } from "next/server";
import { cookies } from "next/headers";

export const dynamic = "force-dynamic";

const inDevEnvironment = !!process && process.env.NODE_ENV === 'development';

export async function GET(request: NextRequest) {
// This route is intended for development/testing purposes only
if (!inDevEnvironment) {
return NextResponse.redirect('/')
}
const requestUrl = new URL(request.url);
// Extract email and password from query parameters
const email = requestUrl.searchParams.get("email");
const password = requestUrl.searchParams.get("password");
if (email && password) {
const supabase = createRouteHandlerClient({ cookies });
// Sign in the user with email and password
await supabase.auth.signInWithPassword({ email, password });
}
return NextResponse.redirect(requestUrl.origin);
}
```

With this setup, we can now easily log in as a user using email and password by navigating to:
`http://localhost:3000/api/auth/dev/login?email=<user-email>&password=<user-password>`

However, we still need to create a new user with email and password. This is where Snaplet Seed will be utilized.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was already in the repo:

So I guessed we don't need it in the readme still. Or did I misunderstand, and we want to still keep it around in the readme regardless?

### Setup @snaplet/seed

To set it up:
Expand Down Expand Up @@ -285,159 +235,129 @@ const seed = await createSeedClient();
// Reset the database, keeping the structure intact
await seed.$resetDatabase()

// Create 3 records in the HttpResponses table
await seed.HttpResponses(x => x(3))
// ...
```

Now, let's edit our `seed.ts` file to generate some tweets:

```ts
await seed.$resetDatabase()

// Generate 10 tweets
await seed.tweets(x => x(10))
```
### Writing the Seed Script

After running `npx tsx seed.ts`, we encounter an error related to invalid `avatar_url` in the Next.js images. To fix this, we adjust the `avatar_url` generation in our `seed.ts`:
First we need to change `seed.ts` to create some users using the Supabase SDK.

```ts
import { faker } from '@snaplet/copycat';

const seed = await createSeedClient({
models: {
profiles: {
data: {
avatarUrl: ({ seed }) => faker.image.avatarGitHub(),
import { createSeedClient } from '@snaplet/seed';
import { copycat } from '@snaplet/copycat';
import { createClient } from '@supabase/supabase-js';
import { Database } from './lib/database.types'

const main = () => {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
// Note you might want to use `SUPABASE_ROLE` key here with `auth.admin.signUp` if your app is using email confirmation
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

const PASSWORD = "testuser";

for (let i = 0; i < 5; i++) {
const email = copycat.email(i).toLowerCase();
const avatar = faker.image.avatarGitHub();
const fullName = copycat.fullName(i);
const userName = copycat.username(i);

await supabase.auth.signUp({
email,
password: PASSWORD,
options: {
data: {
avatar_url: avatar,
name: fullName,
user_name: userName,
}
}
}
});
}
});

await seed.$resetDatabase()
}

// Generate 10 tweets with valid avatar URLs
await seed.tweets(x => x(10))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running this script failed for me. The db relies on a users metadata field being populated with the avatarUrl, name, user_name, so that the trigger can use these fields to create the corresponding profile: https://github.com/snaplet/examples/blob/main/seed/supabase-twitter-clone/supabase/migrations/20240312180754_remote_schema.sql#L41-L43

I tried create this metadata field, it worked but then the next error was that @snaplet/seed tried automatically create a profile rows when creating these tweets, so we ended up with duplicate ids (since db created some of the ids via trigger, and @snaplet/seed also did).

At this point though, I figured if the next thing we're doing is showing users a how to create the users+profiles using the supabase sdk, then using connect to connect the profiles up to the tweets, then maybe we just show them that flow, and this first example where we create the tweets without the users isn't needed.

So I changed the readme accordingly.

main()
```

We can now re-run our script with `npx tsx seed.ts`.

Refreshing our page should now display the seeded tweet data correctly.
This process creates a pool of 5 users with email and password logins, allowing us to easily log in as any tweet creator. It will also create the corresponding rows in the `profiles` table.

To easily log in as the creators of these tweets, we integrate the Supabase SDK into our seed script:
Now that we have this profile data, we can create some tweets using `@snaplet/seed`, and connect them up to these profiles:

```ts
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
// Note you might want to use `SUPABASE_ROLE` key here with `auth.admin.signUp` if your app is using email confirmation
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

const PASSWORD = "testuser";
for (let i = 0; i < 5; i++) {
const email = copycat.email(i).toLowerCase();
const avatar = faker.image.avatarGitHub();
const fullName = copycat.fullName(i);
const userName = copycat.username(i);

await supabase.auth.signUp({
email,
password: PASSWORD,
options: {
data: {
avatar_url: avatar,
name: fullName,
user_name: userName,
}
}
});
}

const { data: databaseProfiles } = await supabase.from("profiles").select();
// * In our app, all our data under public isn't directly linked under the auth.user table but rather under the
// public.profiles table.
// * For any user inserted in the auth.users table we have a trigger that will insert a row in the public.profiles table
// * Since `supabase.auth.signUp()` created a user, we should now have all the profiles created as well
const { data: databaseProfiles } = await supabase.from("profiles").select()

const profiles = databaseProfiles?.map(profile => ({
avatarUrl: profile.avatar_url,
id: profile.id,
name: profile.name,
username: profile.username,
})) ?? [];
// We convert our database fields to something that our seed client can understand
const profiles = databaseProfiles?.map(profile => ({ id: profile.id })) ?? [];

// Insert tweets linked to profiles
await seed.tweets(x => x(10), { connect: { profiles } });
console.log("Profiles created: ", profiles);
// We can now use our seed client to insert tweets that will be linked to the profiles
await seed.tweets(x => x(10), {connect: { profiles }})
```

This process creates a pool of 5 users with email and password logins, allowing us to easily log in as any tweet creator.

Combining all the steps, our `seed.ts` file becomes:

<details>
<summary>Click to show the full code</summary>

```ts
import { createSeedClient, type profilesScalars } from '@snaplet/seed';
import { createClient } from '@supabase/supabase-js'
import {Database} from './lib/database.types'
import { copycat, faker } from '@snaplet/copycat'


const seed = await createSeedClient({
models: {
profiles: {
data: {
avatarUrl: ({ seed }) => faker.image.avatarGitHub(),
}
}
import { createSeedClient } from '@snaplet/seed';
import { faker, copycat } from '@snaplet/copycat';
import { createClient } from '@supabase/supabase-js';
import { Database } from './lib/database.types';

const main = async () => {
const seed = await createSeedClient();

const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
// Note you might want to use `SUPABASE_ROLE` key here with `auth.admin.signUp` if your app is using email confirmation
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

const PASSWORD = 'testuser';
for (let i = 0; i < 5; i++) {
const email = copycat.email(i).toLowerCase();
const avatar = faker.image.avatarGitHub();
const fullName = copycat.fullName(i);
const userName = copycat.username(i);

await supabase.auth.signUp({
email,
password: PASSWORD,
options: {
data: {
avatar_url: avatar,
name: fullName,
user_name: userName,
},
},
});
}
});

const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
const { data: databaseProfiles } = await supabase.from('profiles').select();

const profiles =
databaseProfiles?.map((profile) => ({ id: profile.id })) ?? [];

// Insert tweets linked to profiles
await seed.tweets((x) => x(10), { connect: { profiles } });

// Clears all existing data in the database, but keep the structure
await seed.$resetDatabase()
// Type completion not working? You might want to reload your TypeScript Server to pick up the changes

const PASSWORD = "testuser";
for (let i = 0; i < 5; i += 1) {
const email = copycat.email(i).toLowerCase();
const avatar: string = faker.image.avatarGitHub();
const fullName: string = copycat.fullName(i);
const userName: string = copycat.username(i);
await supabase.auth.signUp({
email,
password: PASSWORD,
options: {
data: {
avatar_url: avatar,
name: fullName,
user_name: userName,
}
}
});
}
// In our app, all our data under public isn't directly linked under the auth.user table but rather under the public.profiles table
// And for any user inserted in the auth.users table we have a trigger that will insert a row in the public.profiles table
// Since `supabase.auth.signUp` create a user, we should now have all the profiles created as well
const { data: databaseProfiles } = await supabase.from("profiles").select()
// We convert our database fields to something that our seed client can understand
const profiles: profilesScalars[] = databaseProfiles?.map(profile => ({
avatarUrl: profile.avatar_url,
id: profile.id,
name: profile.name,
username: profile.username,
})) ?? []
console.log('Database seeded successfully!');

// We can now use our seed client to insert tweets that will be linked to the profiles
await seed.tweets(x => x(10), {connect: { profiles }})
console.log('Profiles created: ', profiles)
```
process.exit();
};

main();
```
</details>

Re-run the seed script with the environment variables set to your local Supabase instance:
We can now run the seed script with the environment variables set to your local Supabase instance:

`NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key> npx tsx seed.ts`:

Expand Down