Skip to content

Commit 1feb3b5

Browse files
committed
Switch to server components and actions
1 parent e54e840 commit 1feb3b5

File tree

8 files changed

+616
-944
lines changed

8 files changed

+616
-944
lines changed

docs/intro/quickstart/access.rst

Lines changed: 208 additions & 287 deletions
Large diffs are not rendered by default.

docs/intro/quickstart/dynamic.rst

Lines changed: 108 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,122 +4,125 @@
44
Dynamic Queries
55
===============
66

7-
.. edb:split-section::
8-
9-
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.
10-
11-
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.
7+
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:
128

13-
.. code-block:: sh
14-
15-
$ npx @gel/generate edgeql-js
9+
1. Write a single complex query that conditionally handles optional parameters
10+
2. Build the query dynamically in the application code based on which fields need updating
1611

12+
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.
1713

1814
.. edb:split-section::
1915
20-
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``.
21-
22-
.. code-block:: typescript-diff
23-
:caption: app/api/deck/[id]/route.ts
24-
25-
import { NextRequest, NextResponse } from "next/server";
26-
import { getAuthenticatedClient } from "@/lib/gel";
27-
+ import e from "@/dbschema/edgeql-js";
28-
29-
import { getDeck } from "./get-deck.query";
30-
31-
interface GetDeckSuccessResponse {
32-
id: string;
33-
name: string;
34-
description: string | null;
35-
creator: {
36-
id: string;
37-
name: string;
38-
} | null;
39-
cards: {
40-
id: string;
41-
front: string;
42-
back: string;
43-
}[];
44-
}
16+
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.
4517

46-
interface GetDeckErrorResponse {
47-
error: string;
48-
}
49-
50-
type GetDeckResponse = GetDeckSuccessResponse | GetDeckErrorResponse;
18+
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.
5119

52-
export async function GET(
53-
req: NextRequest,
54-
{ params }: { params: Promise<{ id: string }> }
55-
): Promise<NextResponse<GetDeckResponse>> {
56-
const client = getAuthenticatedClient(req);
20+
.. code-block:: typescript
21+
:caption: app/deck/[id]/actions.ts
5722
58-
if (!client) {
59-
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
60-
}
23+
"use server";
6124
62-
const { id: deckId } = await params;
63-
const deck = await getDeck(client, { deckId });
25+
import { revalidatePath } from "next/cache";
26+
import e from "@/dbschema/edgeql-js";
6427
65-
if (!deck) {
66-
return NextResponse.json(
67-
{ error: `Deck (${deckId}) not found` },
68-
{ status: 404 }
69-
);
28+
export async function updateDeck(data: FormData) {
29+
const id = data.get("id");
30+
if (!id) {
31+
return;
7032
}
7133
72-
return NextResponse.json(deck);
34+
const name = data.get("name");
35+
const description = data.get("description");
36+
37+
const nameSet = typeof name === "string" ? { name } : {};
38+
const descriptionSet =
39+
typeof description === "string"
40+
? { description: description || null }
41+
: {};
42+
43+
await e
44+
.update(e.Deck, (d) => ({
45+
filter_single: e.op(d.id, "=", id),
46+
set: {
47+
...nameSet,
48+
...descriptionSet,
49+
},
50+
}))
51+
.run(client);
52+
53+
revalidatePath(`/deck/${id}`);
7354
}
7455
75-
+ interface UpdateDeckBody {
76-
+ name?: string;
77-
+ description?: string | null;
78-
+ }
79-
+
80-
+ interface UpdateDeckSuccessResponse {
81-
+ id: string;
82-
+ }
83-
+
84-
+ interface UpdateDeckErrorResponse {
85-
+ error: string;
86-
+ }
87-
+
88-
+ type UpdateDeckResponse = UpdateDeckSuccessResponse | UpdateDeckErrorResponse;
89-
+
90-
+ export async function PATCH(
91-
+ req: NextRequest,
92-
+ { params }: { params: Promise<{ id: string }> }
93-
+ ): Promise<NextResponse<UpdateDeckResponse>> {
94-
+ const client = getAuthenticatedClient(req);
95-
+
96-
+ if (!client) {
97-
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
98-
+ }
99-
+
100-
+ const { id: deckId } = await params;
101-
+ const body = (await req.json()) as UpdateDeckBody;
102-
+
103-
+ const nameSet = body.name !== undefined ? { name: body.name } : {};
104-
+ const descriptionSet =
105-
+ body.description !== undefined ? { description: body.description } : {};
106-
+
107-
+ const updated = await e
108-
+ .update(e.Deck, (deck) => ({
109-
+ filter_single: e.op(deck.id, "=", deckId),
110-
+ set: {
111-
+ ...nameSet,
112-
+ ...descriptionSet,
113-
+ },
114-
+ }))
115-
+ .run(client);
116-
+
117-
+ if (!updated) {
118-
+ return NextResponse.json(
119-
+ { error: `Deck (${deckId}) not found` },
120-
+ { status: 404 }
121-
+ );
122-
+ }
123-
+
124-
+ return NextResponse.json(updated);
125-
+ }
56+
.. code-tab:: typescript-diff
57+
:caption: app/deck/[id]/page.tsx
58+
59+
import { redirect } from "next/navigation";
60+
import { getAuthenticatedClient } from "@/lib/gel";
61+
import e from "@/dbschema/edgeql-js";
62+
+ import { updateDeck } from "./actions";
63+
64+
const getDeckQuery = e.params({ deckId: e.uuid }, (params) =>
65+
e.select(e.Deck, (d) => ({
66+
filter_single: e.op(d.id, "=", params.deckId),
67+
id: true,
68+
name: true,
69+
description: true,
70+
cards: {
71+
id: true,
72+
front: true,
73+
back: true,
74+
order: true,
75+
},
76+
creator: {
77+
id: true,
78+
name: true,
79+
},
80+
}))
81+
);
82+
83+
export default async function DeckPage(
84+
{ params }: { params: Promise<{ id: string }> }
85+
) {
86+
const { id: deckId } = await params;
87+
const client = await getAuthenticatedClient();
88+
if (!client) {
89+
redirect("/signup");
90+
}
91+
92+
const deck = await getDeckQuery.run(client, { deckId });
93+
94+
if (!deck) {
95+
redirect("/");
96+
}
97+
98+
return (
99+
<div>
100+
- <h1>{deck.name}</h1>
101+
- <p>{deck.description}</p>
102+
+ <form action={updateDeck}>
103+
+ <input
104+
+ type="hidden"
105+
+ name="id"
106+
+ value={deck.id}
107+
+ />
108+
+ <input
109+
+ name="name"
110+
+ initialValue={deck.name}
111+
+ />
112+
+ <textarea
113+
+ name="description"
114+
+ initialValue={deck.description}
115+
+ />
116+
+ <button type="submit">Update</button>
117+
+ </form>
118+
<ul>
119+
{deck.cards.map((card) => (
120+
<dl key={card.id}>
121+
<dt>{card.front}</dt>
122+
<dd>{card.back}</dd>
123+
</dl>
124+
))}
125+
</ul>
126+
</div>
127+
)
128+
}

docs/intro/quickstart/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Our Flashcards app will be a modern web application with the following features:
2626
* Create, edit and delete flashcard decks
2727
* Add and remove cards from decks
2828
* Display cards with front/back text content
29-
* Simple HTTP API for managing cards and decks
29+
* Simple UI with Next.js and Tailwind CSS
3030
* Clean, type-safe data modeling using Gel's schema system
3131

3232
Before you start, you'll need:

docs/intro/quickstart/inheritance.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ Adding Shared Properties
2929
- type User {
3030
+ type User extends Timestamped {
3131
required name: str;
32-
33-
tokens := (select .<user[is AccessToken]);
3432
}
3533
3634
- type AccessToken {
@@ -75,7 +73,7 @@ Adding Shared Properties
7573
7674
.. edb:split-section::
7775
78-
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.
76+
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()``.
7977

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

89+
.. code-block:: sh
90+
91+
$ echo

docs/intro/quickstart/modeling.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Modeling our data
66

77
.. edb:split-section::
88
9-
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.
9+
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.
1010

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

docs/intro/quickstart/setup.rst

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@ Setting up your environment
66

77
.. edb:split-section::
88
9-
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.
10-
11-
.. note::
12-
13-
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>`_.
9+
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.
1410

1511
.. code-block:: sh
1612
17-
$ npm create @gel \
18-
--environment=nextjs \
19-
--project-name=flashcards --yes
13+
$ git clone \
14+
git@github.com:geldata/quickstart-nextjs.git \
15+
flashcards
2016
$ cd flashcards
17+
$ npm install
2118
$ npm run test
2219
2320
@@ -40,16 +37,16 @@ Setting up your environment
4037
db> select sum({1, 2, 3});
4138
{6}
4239
db> with cards := {
43-
(
44-
front := "What is the highest mountain in the world?",
45-
back := "Mount Everest",
46-
),
47-
(
48-
front := "Which ocean contains the deepest trench on Earth?",
49-
back := "The Pacific Ocean",
50-
),
51-
}
52-
select cards order by random() limit 1;
40+
... (
41+
... front := "What is the highest mountain in the world?",
42+
... back := "Mount Everest",
43+
... ),
44+
... (
45+
... front := "Which ocean contains the deepest trench on Earth?",
46+
... back := "The Pacific Ocean",
47+
... ),
48+
... }
49+
... select cards order by random() limit 1;
5350
{
5451
(
5552
front := "What is the highest mountain in the world?",
@@ -59,9 +56,9 @@ Setting up your environment
5956
6057
.. edb:split-section::
6158
62-
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.
59+
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.
6360

64-
- ``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.
61+
- ``gel.toml``: This is the configuration file for the Gel project instance.
6562
- ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code.
6663
- ``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.
6764
- ``lib/gel.ts``: This file contains the Gel client, which we'll use to interact with the database.

0 commit comments

Comments
 (0)