From 0182f1cac67231ae2a3ff7e6f72f69d1f364d24c Mon Sep 17 00:00:00 2001 From: Scott Trinh Date: Thu, 13 Feb 2025 16:15:20 -0500 Subject: [PATCH] wip --- docs/intro/quickstart/access.rst | 423 --------------- docs/intro/quickstart/dynamic.rst | 136 ----- docs/intro/quickstart/index.rst | 5 +- docs/intro/quickstart/inheritance.rst | 89 ++-- docs/intro/quickstart/modeling.rst | 40 +- docs/intro/quickstart/setup.rst | 10 +- docs/intro/quickstart/workflow.rst | 111 ---- docs/intro/quickstart/working.rst | 717 +++++++++++++------------- 8 files changed, 447 insertions(+), 1084 deletions(-) delete mode 100644 docs/intro/quickstart/access.rst delete mode 100644 docs/intro/quickstart/dynamic.rst delete mode 100644 docs/intro/quickstart/workflow.rst diff --git a/docs/intro/quickstart/access.rst b/docs/intro/quickstart/access.rst deleted file mode 100644 index a41e946534e..00000000000 --- a/docs/intro/quickstart/access.rst +++ /dev/null @@ -1,423 +0,0 @@ -.. _ref_quickstart_access: - -===================== -Adding Access Control -===================== - -.. edb:split-section:: - - In this section, you will add a concept of a user to your application, and update your data model to limit access to the decks and cards to only the user's own decks. The ``User`` type will be very simple, and for authentication, use a simple ``AccessToken`` type that gets returned from the user creation endpoint when you make a new user. Gel has some really powerful tools available in our authentication extension, but for now, just use a simple token that you will store in the database. - - Along with this user type, add some ``global`` values that will use the access token provided by the client to set a global ``current_user`` variable that you can use in your queries to limit access to the decks and cards to only the user's own decks. - - .. note:: - - Deck creators should be required, but since you are adding this to an existing dataset, set the new ``creator`` property to optional. That will effectively make the existing cards and decks invisible since they don't have a creator. You can update the existing data in the database to set the ``creator`` property for all of the existing decks and cards after making the first user, or reinsert the deck and the creator will be set in your updated query. - - .. code-block:: sdl-diff - :caption: dbschema/default.gel - - module default { - + single optional global access_token: uuid; - + single optional global current_user := ( - + select AccessToken filter .id = global access_token - + ).user; - + - + type User { - + required name: str; - + } - + - + type AccessToken { - + required user: User; - + } - + - type Deck { - required name: str; - description: str; - + - + creator: User; - - cards := (select . y - did you create object type 'default::User'? [y,n,l,c,b,s,q,?] - > y - did you create object type 'default::AccessToken'? [y,n,l,c,b,s,q,?] - > y - did you create global 'default::current_user'? [y,n,l,c,b,s,q,?] - > y - did you alter object type 'default::Deck'? [y,n,l,c,b,s,q,?] - > y - did you create access policy 'deck_creator_has_full_access' of object type 'default::Card'? [y,n,l,c,b,s,q,?] - > y - Created /home/strinh/projects/flashcards/dbschema/migrations/00003-m1solvt.edgeql, id: m1solvta35uzsbs4axzqmkwfx7zatjtkozpr43cjs56fp75qzbrg5q - - $ npx gel migrate - Applying m1solvta35uzsbs4axzqmkwfx7zatjtkozpr43cjs56fp75qzbrg5q (00003-m1solvt.edgeql) - ... parsed - ... applied - Generating query builder... - Detected tsconfig.json, generating TypeScript files. - To override this, use the --target flag. - Run `npx @gel/generate --help` for full options. - Introspecting database schema... - Generating runtime spec... - Generating cast maps... - Generating scalars... - Generating object types... - Generating function types... - Generating operators... - Generating set impl... - Generating globals... - Generating index... - Writing files to ./dbschema/edgeql-js - Generation complete! 🤘 - -.. edb:split-section:: - - Create a page for creating a new user and getting an access token. Start by creating the query to create a new user which will return the ``AccessToken.id`` which you will use as the access token itself. Save this access token in a cookie so that you can authenticate requests in other server actions and route handlers. - - .. tabs:: - - .. code-tab:: typescript - :caption: app/signup/actions.ts - - "use server"; - - import { redirect } from "next/navigation"; - import { cookies } from "next/headers"; - - import { client } from "@/lib/gel"; - import e from "@/dbschema/edgeql-js"; - - const createUser = e.params( - { - name: e.str, - }, - (params) => - e.insert(e.AccessToken, { - user: e.insert(e.User, { name: params.name }), - }) - ); - - export async function signUp(formData: FormData) { - const name = formData.get("name"); - if (typeof name !== "string") { - console.error("Name is required"); - return; - } - - const access_token = await createUser(client, { name }); - (await cookies()).set("flashcards_access_token", access_token.id); - redirect("/"); - } - - - .. code-tab:: typescript - :caption: app/signup/page.tsx - - import { Button } from "@/components/ui/button"; - import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - } from "@/components/ui/card"; - import { Input } from "@/components/ui/input"; - import { Label } from "@/components/ui/label"; - - import { signUp } from "./actions"; - - export default function SignUpPage() { - return ( -
- - - Sign Up - - Enter your name below to create an account - - - -
-
-
- - -
- -
-
-
-
-
- ); - } - -.. edb:split-section:: - - You should see this page when you navigate to the signup page. - - .. code-block:: sh - - $ echo - -Limiting access -=============== - -.. edb:split-section:: - - Now that you have your access token in a cookie, create a helper function to extract it and add it as a global to your client. - - .. code-block:: typescript-diff - :caption: app/lib/gel.ts - - + import { createClient, type Client } from "gel"; - - import { createClient } from "gel"; - + import { cookies } from "next/headers"; - - export const client = createClient(); - - + export async function getAuthenticatedClient(): Promise { - + const access_token = (await cookies()).get("flashcards_access_token")?.value; - + if (!access_token) { - + return null; - + } - + return client.withGlobals({ access_token }); - + } - -.. edb:split-section:: - - Along with allowing you to take advantage of your access policies in your queries, this will also allow you to redirect unauthenticated users to the signup page from any of your pages which should require authentication. Update your ``page.tsx`` file to redirect to the signup page if the user is not authenticated. Also, show the list of decks on this page. - - .. tabs:: - - .. code-tab:: typescript-diff - :caption: app/actions.ts - - "use server"; - - import { client } from "@/lib/gel"; - + import { getAuthenticatedClient } from "@/lib/gel"; - import { createDeck } from "./create-deck.query"; - + import e from "@/dbschema/edgeql-js"; - - export async function importDeck(formData: FormData) { - const deck = formData.get("deck"); - if (typeof deck !== "string") { - return; - } - + - + const client = await getAuthenticatedClient(); - + if (!client) { - + return; - + } - - await createDeck(client, JSON.parse(deck)); - } - + - + export async function getDecks() { - + const client = await getAuthenticatedClient(); - + if (!client) { - + return []; - + } - + - + return e.select(e.Deck, (d) => ({ - + id: true, - + name: true, - + })).run(client); - + } - - .. code-tab:: typescript-diff - :caption: app/page.tsx - - import { ImportForm } from "./form"; - + import { getAuthenticatedClient } from "@/lib/gel"; - + import { redirect } from "next/navigation"; - + import { getDecks } from "./actions"; - - export default async function Page() { - + const client = await getAuthenticatedClient(); - + if (!client) { - + redirect("/signup"); - + } - + - + const decks = await getDecks(client); - + - - return ; - + return ( - +
- +

Decks

- +
    - + {decks.map((deck) => ( - +
  • {deck.name}
  • - + ))} - +
- + - +
- + ); - } - -.. edb:split-section:: - - Next update the create deck query and server action with the authentication logic and ``creator`` property. - - .. tabs:: - - .. code-tab:: typescript-diff - :caption: app/actions.ts - - "use server"; - import { redirect } from "next/navigation"; - - import { client } from "@/lib/gel"; - + import { getAuthenticatedClient } from "@/lib/gel"; - import { createDeck } from "./create-deck.query"; - - export async function createDeck(formData: FormData) { - const deck = formData.get("deck"); - if (typeof deck !== "string") { - return; - } - - const client = await getAuthenticatedClient(); - if (!client) { - return; - } - - const { id } = await createDeck(client, JSON.parse(deck)); - redirect(`/deck/${id}`); - } - - .. code-tab:: typescript-diff - :caption: app/create-deck.query.ts (query builder) - - // Run `npm generate edgeql-js` to generate the `e` query builder module. - import e from "@/dbschema/edgeql-js"; - - const createDeckQuery = e.params( - { - name: e.str, - description: e.optional(e.str), - cards: e.array(e.tuple({ order: e.int64, front: e.str, back: e.str })), - }, - ({ - cards, - ...deckData - }) => { - - const newDeck = e.insert(e.Deck, deckData); - + const newDeck = e.insert(e.Deck, { - + ...deckData, - + creator: e.assert_exists(e.global.current_user), - + }); - const newCards = e.for(e.array_unpack(cards), (card) => - e.insert(e.Card, { - ...card, - deck: newDeck, - }) - ); - return e.with([newCards], e.select(newDeck)); - } - ); - - export const createDeck = createDeckQuery.run.bind(createDeckQuery); - -.. edb:split-section:: - - Finally, update the deck page to require an authenticated user, and to return the deck's creator. - - .. code-block:: typescript-diff - :caption: app/deck/[id]/page.tsx - - import { notFound } from "next/navigation"; - - import { client } from "@/lib/gel"; - + import { getAuthenticatedClient } from "@/lib/gel"; - import e from "@/dbschema/edgeql-js"; - import { Fragment } from "react"; - - const getDeckQuery = e.params({ id: e.uuid }, (params) => - e.select(e.Deck, (d) => ({ - filter_single: e.op(d.id, "=", params.id), - 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 } = await params; - + const client = await getAuthenticatedClient(); - + if (!client) { - + notFound(); - + } - + - const deck = await getDeckQuery.run(client, { id }); - - if (!deck) { - notFound(); - } - - return ( -
-

{deck.name}

-

{deck.description}

-
- {deck.cards.map((card) => ( - -
{card.front}
-
{card.back}
-
- ))} -
-
- ) - } diff --git a/docs/intro/quickstart/dynamic.rst b/docs/intro/quickstart/dynamic.rst deleted file mode 100644 index eed1a985d5d..00000000000 --- a/docs/intro/quickstart/dynamic.rst +++ /dev/null @@ -1,136 +0,0 @@ -.. _ref_quickstart_dynamic: - -=============== -Dynamic Queries -=============== - -When updating data, you often want to modify only specific fields while leaving others unchanged. For example, you 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:: - - Create a server action that updates a deck's ``name`` and/or ``description``. Since the description is optional, treat clearing the ``description`` form field as unsetting the ``description`` property. - - Update the deck page to allow updating a deck's ``name`` and/or ``description``. Treat the request body as a partial update, and only update the fields that are provided. Since the description is optional, treat clearing the ``description`` form field as unsetting the ``description`` property. - - .. tabs:: - - .. code-tab:: typescript - :caption: app/deck/[id]/actions.ts - - "use server"; - - import { revalidatePath } from "next/cache"; - import e from "@/dbschema/edgeql-js"; - import { getAuthenticatedClient } from "@/lib/gel"; - - export async function updateDeck(data: FormData) { - const id = data.get("id"); - if (!id) { - throw new Error("Missing deck ID"); - } - - const client = await getAuthenticatedClient(); - if (!client) { - throw new Error("Unauthorized"); - } - - const name = data.get("name"); - const description = data.get("description"); - - const nameSet = typeof name === "string" ? { name } : {}; - const descriptionSet = - typeof description === "string" - ? { description: description || null } - : {}; - - await e - .update(e.Deck, (d) => ({ - filter_single: e.op(d.id, "=", e.uuid(id)), - set: { - ...nameSet, - ...descriptionSet, - }, - })) - .run(client); - - revalidatePath(`/deck/${id}`); - } - - .. 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 ( -
- -

{deck.name}

- -

{deck.description}

- +
- + - + - +