|
4 | 4 | Dynamic Queries |
5 | 5 | =============== |
6 | 6 |
|
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: |
12 | 8 |
|
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 |
16 | 11 |
|
| 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. |
17 | 13 |
|
18 | 14 | .. edb:split-section:: |
19 | 15 |
|
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. |
45 | 17 |
|
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. |
51 | 19 |
|
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 |
57 | 22 |
|
58 | | - if (!client) { |
59 | | - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); |
60 | | - } |
| 23 | + "use server"; |
61 | 24 |
|
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"; |
64 | 27 |
|
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; |
70 | 32 | } |
71 | 33 |
|
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}`); |
73 | 54 | } |
74 | 55 |
|
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 | + } |
0 commit comments