Skip to content

Commit

Permalink
Switch to server components and actions
Browse files Browse the repository at this point in the history
  • Loading branch information
scotttrinh committed Feb 6, 2025
1 parent 4fbb0ce commit 9335c11
Show file tree
Hide file tree
Showing 8 changed files with 620 additions and 946 deletions.
495 changes: 208 additions & 287 deletions docs/intro/quickstart/access.rst

Large diffs are not rendered by default.

219 changes: 112 additions & 107 deletions docs/intro/quickstart/dynamic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,122 +4,127 @@
Dynamic Queries
===============

When updating data, we often want to modify only specific fields while leaving others unchanged. For example, we might want to update just the front text of a flashcard or only the description of a deck. There are two main approaches to handle these partial updates:

1. Write a single complex query that conditionally handles optional parameters
2. Build the query dynamically in the application code based on which fields need updating

The second approach using dynamic queries tends to be more performant and maintainable. EdgeDB's TypeScript query builder excels at this use case. It allows you to construct queries dynamically while maintaining full type safety. Let's see how to implement this pattern.

.. edb:split-section::
Maybe we only want to update one side of an existing card, or just edit the description of a deck. One approach is writing a very complicated single query that tries to handle all of the dynamic cases. Another approach is to build the query dynamically in the application code. This has the benefit of often being better for performance, and it's easier to understand and maintain. We provide another very powerful code generator, our TypeScript query builder, that allows you to build queries dynamically in the application code, while giving you strict type safety.
Let's create a server action that updates a deck's ``name`` and/or ``description``. Since the description is optional, we will treat clearing the ``description`` form field as unsetting the ``description`` property.

First, we will generate the query builder. This will generate a module in our ``dbschema`` directory called ``edgeql-js``, which we can import in our route and use to build a dynamic query.
Let's update the deck page to allow updating a deck's ``name`` and/or ``description``. We will treat the request body as a partial update, and only update the fields that are provided. Since the description is optional, we will treat clearing the ``description`` form field as unsetting the ``description`` property.

.. code-block:: sh
.. tabs::

$ npx @gel/generate edgeql-js
.. code-tab:: typescript
:caption: app/deck/[id]/actions.ts

"use server";

.. edb:split-section::
import { revalidatePath } from "next/cache";
import e from "@/dbschema/edgeql-js";

Now let's use the query builder in a new route for updating a deck's ``name`` and/or ``description``. We will treat the request body as a partial update, and only update the fields that are provided. Since the description is optional, we will use a nullable string for the type, so you can "unset" the description by passing in ``null``.

.. code-block:: typescript-diff
:caption: app/api/deck/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAuthenticatedClient } from "@/lib/gel";
+ import e from "@/dbschema/edgeql-js";
import { getDeck } from "./get-deck.query";
interface GetDeckSuccessResponse {
id: string;
name: string;
description: string | null;
creator: {
id: string;
name: string;
} | null;
cards: {
id: string;
front: string;
back: string;
}[];
}
interface GetDeckErrorResponse {
error: string;
}
type GetDeckResponse = GetDeckSuccessResponse | GetDeckErrorResponse;
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
): Promise<NextResponse<GetDeckResponse>> {
const client = getAuthenticatedClient(req);
if (!client) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
export async function updateDeck(data: FormData) {
const id = data.get("id");
if (!id) {
return;
}

const name = data.get("name");
const description = data.get("description");

const { id: deckId } = await params;
const deck = await getDeck(client, { deckId });
const nameSet = typeof name === "string" ? { name } : {};
const descriptionSet =
typeof description === "string"
? { description: description || null }
: {};

if (!deck) {
return NextResponse.json(
{ error: `Deck (${deckId}) not found` },
{ status: 404 }
);
await e
.update(e.Deck, (d) => ({
filter_single: e.op(d.id, "=", id),
set: {
...nameSet,
...descriptionSet,
},
}))
.run(client);

revalidatePath(`/deck/${id}`);
}

return NextResponse.json(deck);
}
+ interface UpdateDeckBody {
+ name?: string;
+ description?: string | null;
+ }
+
+ interface UpdateDeckSuccessResponse {
+ id: string;
+ }
+
+ interface UpdateDeckErrorResponse {
+ error: string;
+ }
+
+ type UpdateDeckResponse = UpdateDeckSuccessResponse | UpdateDeckErrorResponse;
+
+ export async function PATCH(
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+ ): Promise<NextResponse<UpdateDeckResponse>> {
+ const client = getAuthenticatedClient(req);
+
+ if (!client) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { id: deckId } = await params;
+ const body = (await req.json()) as UpdateDeckBody;
+
+ const nameSet = body.name !== undefined ? { name: body.name } : {};
+ const descriptionSet =
+ body.description !== undefined ? { description: body.description } : {};
+
+ const updated = await e
+ .update(e.Deck, (deck) => ({
+ filter_single: e.op(deck.id, "=", deckId),
+ set: {
+ ...nameSet,
+ ...descriptionSet,
+ },
+ }))
+ .run(client);
+
+ if (!updated) {
+ return NextResponse.json(
+ { error: `Deck (${deckId}) not found` },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json(updated);
+ }
.. code-tab:: typescript-diff
:caption: app/deck/[id]/page.tsx

import { redirect } from "next/navigation";
import { getAuthenticatedClient } from "@/lib/gel";
import e from "@/dbschema/edgeql-js";
+ import { updateDeck } from "./actions";

const getDeckQuery = e.params({ deckId: e.uuid }, (params) =>
e.select(e.Deck, (d) => ({
filter_single: e.op(d.id, "=", params.deckId),
id: true,
name: true,
description: true,
cards: {
id: true,
front: true,
back: true,
order: true,
},
creator: {
id: true,
name: true,
},
}))
);

export default async function DeckPage(
{ params }: { params: Promise<{ id: string }> }
) {
const { id: deckId } = await params;
const client = await getAuthenticatedClient();
if (!client) {
redirect("/signup");
}

const deck = await getDeckQuery.run(client, { deckId });

if (!deck) {
redirect("/");
}

return (
<div>
- <h1>{deck.name}</h1>
- <p>{deck.description}</p>
+ <form action={updateDeck}>
+ <input
+ type="hidden"
+ name="id"
+ value={deck.id}
+ />
+ <input
+ name="name"
+ initialValue={deck.name}
+ />
+ <textarea
+ name="description"
+ initialValue={deck.description}
+ />
+ <button type="submit">Update</button>
+ </form>
<ul>
{deck.cards.map((card) => (
<dl key={card.id}>
<dt>{card.front}</dt>
<dd>{card.back}</dd>
</dl>
))}
</ul>
</div>
)
}
2 changes: 1 addition & 1 deletion docs/intro/quickstart/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Our Flashcards app will be a modern web application with the following features:
* Create, edit and delete flashcard decks
* Add and remove cards from decks
* Display cards with front/back text content
* Simple HTTP API for managing cards and decks
* Simple UI with Next.js and Tailwind CSS
* Clean, type-safe data modeling using Gel's schema system

Before you start, you'll need:
Expand Down
7 changes: 4 additions & 3 deletions docs/intro/quickstart/inheritance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ Adding Shared Properties
- type User {
+ type User extends Timestamped {
required name: str;
tokens := (select .<user[is AccessToken]);
}
- type AccessToken {
Expand Down Expand Up @@ -75,7 +73,7 @@ Adding Shared Properties
.. edb:split-section::
This will require that we make a manual migration since we will need to backfill the ``created_at`` and ``updated_at`` properties for all existing objects. We will just set the value to be the current wall time since we do not have a meaningful way to backfill the values for existing objects.
When we create a migration, we need to set initial values for the ``created_at`` and ``updated_at`` properties on all existing objects. Since we don't have historical data for when these objects were actually created or modified, we'll set both timestamps to the current time when the migration runs by using ``datetime_of_statement()``.

.. code-block:: sh
Expand All @@ -88,3 +86,6 @@ Adding Shared Properties
Now when we look at the data in the UI, we will see the new properties on each of our object types.

.. code-block:: sh
$ echo
2 changes: 1 addition & 1 deletion docs/intro/quickstart/modeling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Modeling our data

.. edb:split-section::
Our flashcards application has a simple data model, but it's interesting enough to get a taste of many of the features of the Gel schema language. We have a ``Card`` type that describes an single flashcard, which for now contains two required string properties: ``front`` and ``back``. Each ``Card`` belongs to a ``Deck``, and there is a natural ordering to the cards in a given deck.
Our flashcards application has a simple data model, but it's interesting enough to get a taste of many of the features of the Gel schema language. We have a ``Card`` type that describes an single flashcard, which for now contains two required string properties: ``front`` and ``back``. Each ``Card`` belongs to a ``Deck``, and there is an explicit ordering to the cards in a given deck.

Starting with this simple model, let's express these types in the ``default.gel`` schema file.

Expand Down
37 changes: 17 additions & 20 deletions docs/intro/quickstart/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ Setting up your environment

.. edb:split-section::
We will use our project starter CLI to scaffold our Next.js application with everything we need to get started with Gel. This will create a new directory called ``flashcards`` with a fully configured Next.js project and a local Gel database with an empty schema. You should see the test suite pass, indicating that the database instance was created successfully, and we're ready to start building our application.

.. note::

If you run into any issues at this point, look back at the output of the ``npm create @gel`` command for any error messages. Feel free to ask for help in the `Gel Discord <https://discord.gg/gel>`_.
We will clone our Next.js starter template into a new directory called ``flashcards``. This will create a fully configured Next.js project and a local Gel instance with an empty schema. You should see the test suite pass, indicating that the database instance was created successfully, and we're ready to start building our application.

.. code-block:: sh
$ npm create @gel \
--environment=nextjs \
--project-name=flashcards --yes
$ git clone \
[email protected]:geldata/quickstart-nextjs.git \
flashcards
$ cd flashcards
$ npm install
$ npm run test
Expand All @@ -40,16 +37,16 @@ Setting up your environment
db> select sum({1, 2, 3});
{6}
db> with cards := {
(
front := "What is the highest mountain in the world?",
back := "Mount Everest",
),
(
front := "Which ocean contains the deepest trench on Earth?",
back := "The Pacific Ocean",
),
}
select cards order by random() limit 1;
... (
... front := "What is the highest mountain in the world?",
... back := "Mount Everest",
... ),
... (
... front := "Which ocean contains the deepest trench on Earth?",
... back := "The Pacific Ocean",
... ),
... }
... select cards order by random() limit 1;
{
(
front := "What is the highest mountain in the world?",
Expand All @@ -59,9 +56,9 @@ Setting up your environment
.. edb:split-section::
Fun! We'll create a proper data model for this in the next step, but for now, let's take a look around the project we've just created. Most of the generated files will be familiar to you if you've worked with Next.js before. So let's focus on the new files that were created to integrate Gel.
Fun! We'll create a proper data model for our application in the next step, but for now, let's take a look around the project we've just created. Most of the project files will be familiar to you if you've worked with Next.js before. So let's focus on the new files that integrate Gel.

- ``gel.toml``: This is the configuration file for the Gel database. It contains the configuration for the local database instance, so that if another developer on your team wants to run the project, they can easily do so and have a compatible database version.
- ``gel.toml``: This is the configuration file for the Gel project instance.
- ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code.
- ``dbschema/default.gel``: This is the default schema file that we'll use to define our data model. It is empty for now, but we'll add our data model to this file in the next step.
- ``lib/gel.ts``: This file contains the Gel client, which we'll use to interact with the database.
Expand Down
Loading

0 comments on commit 9335c11

Please sign in to comment.