From 5ca833a67e0afe577e92c61e6076f7ef329cf63a Mon Sep 17 00:00:00 2001 From: Aleksandra Date: Thu, 20 Feb 2025 20:33:29 +0100 Subject: [PATCH] Add FastAPI quickstart (#8347) --- docs/intro/quickstart/connecting/fastapi.rst | 78 +++ docs/intro/quickstart/connecting/index.rst | 7 +- docs/intro/quickstart/inheritance/fastapi.rst | 90 ++++ docs/intro/quickstart/inheritance/index.rst | 7 +- docs/intro/quickstart/modeling/fastapi.rst | 94 ++++ docs/intro/quickstart/modeling/index.rst | 9 +- docs/intro/quickstart/modeling/nextjs.rst | 2 +- docs/intro/quickstart/overview/fastapi.rst | 58 +++ docs/intro/quickstart/overview/index.rst | 9 +- docs/intro/quickstart/setup/fastapi.rst | 82 +++ docs/intro/quickstart/setup/index.rst | 7 +- docs/intro/quickstart/setup/nextjs.rst | 4 +- docs/intro/quickstart/working/fastapi.rst | 473 ++++++++++++++++++ .../working/images/flashcards-api.png | Bin 0 -> 32396 bytes docs/intro/quickstart/working/index.rst | 10 +- 15 files changed, 903 insertions(+), 27 deletions(-) create mode 100644 docs/intro/quickstart/connecting/fastapi.rst create mode 100644 docs/intro/quickstart/inheritance/fastapi.rst create mode 100644 docs/intro/quickstart/modeling/fastapi.rst create mode 100644 docs/intro/quickstart/overview/fastapi.rst create mode 100644 docs/intro/quickstart/setup/fastapi.rst create mode 100644 docs/intro/quickstart/working/fastapi.rst create mode 100644 docs/intro/quickstart/working/images/flashcards-api.png diff --git a/docs/intro/quickstart/connecting/fastapi.rst b/docs/intro/quickstart/connecting/fastapi.rst new file mode 100644 index 00000000000..af928ef09b9 --- /dev/null +++ b/docs/intro/quickstart/connecting/fastapi.rst @@ -0,0 +1,78 @@ +.. _ref_quickstart_fastapi_connecting: + +========================== +Connecting to the database +========================== + +.. edb:split-section:: + + Before diving into the application, let's take a quick look at how to connect to the database from your code. We will intialize a client and use it to make a simple, static query to the database, and log the result to the console. + + .. note:: + + Notice that the ``create_async_client`` function isn't being passed any connection details. With |Gel|, you do not need to come up with your own scheme for how to build the correct database connection credentials and worry about leaking them into your code. You simply use |Gel| "projects" for local development, and set the appropriate environment variables in your deployment environments, and the ``create_async_client`` function knows what to do! + + .. edb:split-point:: + + .. code-block:: python + :caption: ./test.py + + import gel + import asyncio + + async def main(): + client = gel.create_async_client() + result = await client.query_single("select 'Hello from Gel!';") + print(result) + + asyncio.run(main()) + + .. code-block:: sh + + $ python test.py + Hello from Gel! + +.. edb:split-section:: + + In Python, we write EdgeQL queries directly as strings. This gives us the full power and expressiveness of EdgeQL while maintaining type safety through Gel's strict schema. Let's try inserting a few ``Deck`` objects into the database and then selecting them back. + + .. edb:split-point:: + + .. code-block:: python-diff + :caption: ./test.py + + import gel + import asyncio + + async def main(): + client = gel.create_async_client() + - result = await client.query_single("select 'Hello from Gel!';") + - print(result) + + await client.query(""" + + insert Deck { name := "I am one" } + + """) + + + + await client.query(""" + + insert Deck { name := "I am two" } + + """) + + + + decks = await client.query(""" + + select Deck { + + id, + + name + + } + + """) + + + + for deck in decks: + + print(f"ID: {deck.id}, Name: {deck.name}") + + + + await client.query("delete Deck") + + asyncio.run(main()) + + .. code-block:: sh + + $ python test.py + Hello from Gel! + ID: f4cd3e6c-ea75-11ef-83ec-037350ea8a6e, Name: I am one + ID: f4cf27ae-ea75-11ef-83ec-3f7b2fceab24, Name: I am two diff --git a/docs/intro/quickstart/connecting/index.rst b/docs/intro/quickstart/connecting/index.rst index 18bee065359..ac820e5025f 100644 --- a/docs/intro/quickstart/connecting/index.rst +++ b/docs/intro/quickstart/connecting/index.rst @@ -1,11 +1,12 @@ .. edb:env-switcher:: -========== -Connecting -========== +========================== +Connecting to the database +========================== .. toctree:: :maxdepth: 3 :hidden: nextjs + fastapi diff --git a/docs/intro/quickstart/inheritance/fastapi.rst b/docs/intro/quickstart/inheritance/fastapi.rst new file mode 100644 index 00000000000..c386577c28d --- /dev/null +++ b/docs/intro/quickstart/inheritance/fastapi.rst @@ -0,0 +1,90 @@ +.. _ref_quickstart_fastapi_inheritance: + +======================== +Adding shared properties +======================== + +.. edb:split-section:: + + One common pattern in applications is to add shared properties to the schema that are used by multiple objects. For example, you might want to add a ``created_at`` and ``updated_at`` property to every object in your schema. You can do this by adding an abstract type and using it as a mixin for your other object types. + + .. code-block:: sdl-diff + :caption: dbschema/default.gel + + module default { + + abstract type Timestamped { + + required created_at: datetime { + + default := datetime_of_statement(); + + }; + + required updated_at: datetime { + + default := datetime_of_statement(); + + }; + + } + + + - type Deck { + + type Deck extending Timestamped { + required name: str; + description: str; + + cards := ( + select . y + did you alter object type 'default::Card'? [y,n,l,c,b,s,q,?] + > y + did you alter object type 'default::Deck'? [y,n,l,c,b,s,q,?] + > y + Created /home/strinh/projects/flashcards/dbschema/migrations/00004-m1d2m5n.edgeql, id: m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya + + $ gel migrate + Applying m1d2m5n5ajkalyijrxdliioyginonqbtfzihvwdfdmfwodunszstya (00004-m1d2m5n.edgeql) + ... parsed + ... applied + +.. edb:split-section:: + + Update the ``get_decks`` query to sort the decks by ``updated_at`` in descending order. + + .. code-block:: python-diff + :caption: main.py + + @app.get("/decks", response_model=List[Deck]) + async def get_decks(): + decks = await client.query(""" + select Deck { + id, + name, + description, + cards := ( + select .cards { + id, + front, + back + } + order by .order + ) + } + + order by .updated_at desc + """) + return decks diff --git a/docs/intro/quickstart/inheritance/index.rst b/docs/intro/quickstart/inheritance/index.rst index fa24cf5b8dd..8ad77d34c8e 100644 --- a/docs/intro/quickstart/inheritance/index.rst +++ b/docs/intro/quickstart/inheritance/index.rst @@ -1,12 +1,13 @@ .. edb:env-switcher:: -=========== -Inheritance -=========== +======================== +Adding shared properties +======================== .. toctree:: :maxdepth: 3 :hidden: nextjs + fastapi diff --git a/docs/intro/quickstart/modeling/fastapi.rst b/docs/intro/quickstart/modeling/fastapi.rst new file mode 100644 index 00000000000..c1c407ed897 --- /dev/null +++ b/docs/intro/quickstart/modeling/fastapi.rst @@ -0,0 +1,94 @@ +.. _ref_quickstart_fastapi_modeling: + +================= +Modeling the data +================= + +.. edb:split-section:: + + The flashcards application has a simple data model, but it's interesting enough to utilize many unique features of the |Gel| schema language. + + Looking at the mock data in the example JSON file ``./deck-edgeql.json``, you can see this structure in the JSON. There is a ``Card`` class that describes a single flashcard, which contains two required string properties: ``front`` and ``back``. Each ``Deck`` object has zero or more ``Card`` objects in a list. + + .. code-block:: python + + from pydantic import BaseModel + + class CardBase(BaseModel): + front: str + back: str + + class Card(CardBase): + id: str + + class DeckBase(BaseModel): + name: str + description: Optional[str] = None + + class Deck(DeckBase): + id: str + cards: List[Card] + +.. edb:split-section:: + + Starting with this simple model, add these types to the :dotgel:`dbschema/default` schema file. As you can see, the types closely mirror the JSON mock data. + + Also of note, the link between ``Card`` and ``Deck`` objects creates a "1-to-n" relationship, where each ``Deck`` object has a link to zero or more ``Card`` objects. When you query the ``Deck.cards`` link, the cards will be unordered, so the ``Card`` type needs an explicit ``order`` property to allow sorting them at query time. + + By default, when you try to delete an object that is linked to another object, the database will prevent you from doing so. We want to support removing a ``Card``, so we define a deletion policy on the ``cards`` link that allows deleting the target of this link. + + .. code-block:: sdl-diff + :caption: dbschema/default.gel + + module default { + + type Card { + + required order: int64; + + required front: str; + + required back: str; + + }; + + + + type Deck { + + required name: str; + + description: str; + + multi cards: Card { + + constraint exclusive; + + on target delete allow; + + }; + + }; + }; + +.. edb:split-section:: + + Congratulations! This first version of the data model's schema is *stored in a file on disk*. Now you need to signal the database to actually create types for ``Deck`` and ``Card`` in the database. + + To make |Gel| do that, you need to do two quick steps: + + 1. **Create a migration**: a "migration" is a file containing a set of low level instructions that define how the database schema should change. It records any additions, modifications, or deletions to your schema in a way that the database can understand. + + .. note:: + + When you are changing existing schema, the CLI migration tool might ask questions to ensure that it understands your changes exactly. Since the existing schema was empty, the CLI will skip asking any questions and simply create the migration file. + + 2. **Apply the migration**: This executes the migration file on the database, instructing |Gel| to implement the recorded changes in the database. Essentially, this step updates the database structure to match your defined schema, ensuring that the ``Deck`` and ``Card`` types are created and ready for use. + + .. code-block:: sh + + $ uvx gel migration create + Created ./dbschema/migrations/00001-m125ajr.edgeql, id: m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla + $ uvx gel migrate + Applying m125ajrbqp7ov36s7aniefxc376ofxdlketzspy4yddd3hrh4lxmla (00001-m125ajr.edgeql) + ... parsed + ... applied + + +.. edb:split-section:: + + Take a look at the schema you've generated in the built-in database UI. Use this tool to visualize your data model and see the object types and links you've defined. + + .. edb:split-point:: + + .. code-block:: sh + + $ uvx gel ui + + .. image:: images/schema-ui.png diff --git a/docs/intro/quickstart/modeling/index.rst b/docs/intro/quickstart/modeling/index.rst index e1fed28cb1d..64e5ac4a46e 100644 --- a/docs/intro/quickstart/modeling/index.rst +++ b/docs/intro/quickstart/modeling/index.rst @@ -1,13 +1,12 @@ .. edb:env-switcher:: -======== -Modeling -======== +================= +Modeling the data +================= .. toctree:: :maxdepth: 3 :hidden: nextjs - - + fastapi diff --git a/docs/intro/quickstart/modeling/nextjs.rst b/docs/intro/quickstart/modeling/nextjs.rst index ce54e63690d..903d9ffba3d 100644 --- a/docs/intro/quickstart/modeling/nextjs.rst +++ b/docs/intro/quickstart/modeling/nextjs.rst @@ -8,7 +8,7 @@ Modeling the data The flashcards application has a simple data model, but it's interesting enough to utilize many unique features of the |Gel| schema language. - Looking at the mock data in our example JSON file ``./deck-edgeql.json``, you can see this structure in the JSON. There is a ``Card`` type that describes a single flashcard, which contains two required string properties: ``front`` and ``back``. Each ``Deck`` object has a link to zero or more ``Card`` objects in an array. + Looking at the mock data in the example JSON file ``./deck-edgeql.json``, you can see this structure in the JSON. There is a ``Card`` type that describes a single flashcard, which contains two required string properties: ``front`` and ``back``. Each ``Deck`` object has zero or more ``Card`` objects in an array. .. code-block:: typescript diff --git a/docs/intro/quickstart/overview/fastapi.rst b/docs/intro/quickstart/overview/fastapi.rst new file mode 100644 index 00000000000..d12d835ee71 --- /dev/null +++ b/docs/intro/quickstart/overview/fastapi.rst @@ -0,0 +1,58 @@ +.. _ref_quickstart_fastapi: + +========== +Quickstart +========== + +Welcome to the quickstart tutorial! In this tutorial, you will update a FastAPI +backend for a Flashcards application to use |Gel| as your data layer. The +application will let users build and manage their own study decks, with each +flashcard featuring customizable text on both sides - making it perfect for +studying, memorization practice, or creating educational games. + +Don't worry if you're new to |Gel| - you will be up and running with a working +FastAPI backend and a local |Gel| database in just about **5 minutes**. From +there, you will replace the static mock data with a |Gel| powered data layer in +roughly 30-45 minutes. + +By the end of this tutorial, you will be comfortable with: + +* Creating and updating a database schema +* Running migrations to evolve your data +* Writing EdgeQL queries +* Building an app backed by |Gel| + + +Features of the flashcards app +------------------------------ + +* Create, edit, and delete decks +* Add/remove cards with front/back content +* Clean, type-safe schema with |Gel| + +Requirements +------------ + +Before you start, you need: + +* Basic familiarity with Python and FastAPI +* Python 3.8+ on a Unix-like OS (Linux, macOS, or WSL) +* A code editor you love + +Why |Gel| for FastAPI? +---------------------- + +* **Type Safety**: Catch data errors before runtime +* **Rich Modeling**: Use object types and links to model relations +* **Modern Tooling**: Python-friendly schemas and migrations +* **Performance**: Efficient queries for complex data +* **Developer Experience**: An intuitive query language (EdgeQL) + +Need Help? +---------- + +If you run into issues while following this tutorial: + +- Check the `|Gel| documentation `_ +- Visit our `community Discord `_ +- File an issue on `GitHub `_ diff --git a/docs/intro/quickstart/overview/index.rst b/docs/intro/quickstart/overview/index.rst index bdf8f1e098b..ce33c92eef9 100644 --- a/docs/intro/quickstart/overview/index.rst +++ b/docs/intro/quickstart/overview/index.rst @@ -1,11 +1,12 @@ .. edb:env-switcher:: -======== -Overview -======== +========== +Quickstart +========== .. toctree:: :maxdepth: 3 :hidden: - nextjs \ No newline at end of file + nextjs + fastapi diff --git a/docs/intro/quickstart/setup/fastapi.rst b/docs/intro/quickstart/setup/fastapi.rst new file mode 100644 index 00000000000..72de1479320 --- /dev/null +++ b/docs/intro/quickstart/setup/fastapi.rst @@ -0,0 +1,82 @@ +.. _ref_quickstart_fastapi_setup: + +=========================== +Setting up your environment +=========================== + +.. edb:split-section:: + + Use git to clone the `FastAPI starter template `_ into a new directory called ``flashcards``. This will create a fully configured FastAPI project and a local |Gel| instance with an empty schema. You will see the database instance being created and the project being initialized. You are now ready to start building the application. + + .. code-block:: sh + + $ git clone \ + git@github.com:geldata/quickstart-fastapi.git \ + flashcards + $ cd flashcards + $ python -m venv venv + $ source venv/bin/activate # or venv\Scripts\activate on Windows + $ pip install -r requirements.txt + $ uvx gel project init + +.. edb:split-section:: + + Explore the empty database by starting our REPL from the project root. + + .. code-block:: sh + + $ uvx gel + +.. edb:split-section:: + + Try the following queries which will work without any schema defined. + + .. code-block:: edgeql-repl + + db> select 42; + {42} + 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", + ) + } + +.. edb:split-section:: + + Fun! You will create a proper data model for the application in the next step, but for now, take a look around the project we have. Here are the files that integrate |Gel|: + + - ``gel.toml``: The configuration file for the |Gel| project instance. Notice that we have a ``hooks.migration.apply.after`` hook that will run ``uvx gel-py`` after migrations are applied. This will run the code generator that you will use later to get fully type-safe queries you can run from your FastAPI backend. More details on that to come! + - ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code. + - :dotgel:`dbschema/default`: The default schema file that you'll use to define your data model. It is empty for now, but you'll add your data model to this file in the next step. + + .. tabs:: + + .. code-tab:: toml + :caption: gel.toml + + [instance] + server-version = 6.0 + + [hooks] + schema.update.after = "uvx gel-py" + + .. code-tab:: sdl + :caption: dbschema/default.gel + + module default { + + } diff --git a/docs/intro/quickstart/setup/index.rst b/docs/intro/quickstart/setup/index.rst index 41f28c1b6d6..0135167bee7 100644 --- a/docs/intro/quickstart/setup/index.rst +++ b/docs/intro/quickstart/setup/index.rst @@ -1,11 +1,12 @@ .. edb:env-switcher:: -===== -Setup -===== +=========================== +Setting up your environment +=========================== .. toctree:: :maxdepth: 3 :hidden: nextjs + fastapi diff --git a/docs/intro/quickstart/setup/nextjs.rst b/docs/intro/quickstart/setup/nextjs.rst index 371fb5bd57d..578d4433996 100644 --- a/docs/intro/quickstart/setup/nextjs.rst +++ b/docs/intro/quickstart/setup/nextjs.rst @@ -58,10 +58,10 @@ Setting up your environment Fun! You will create a proper data model for the application in the next step, but for now, take a look around the project you've just created. Most of the project files will be familiar if you've worked with Next.js before. Here are the files that integrate |Gel|: - - ``gel.toml``: The configuration file for the Gel project instance. Notice that we have a ``hooks.migration.apply.after`` hook that will run ``npx @gel/generate edgeql-js`` after migrations are applied. This will generate the query builder code that you'll use to interact with the database. More details on that to come! + - ``gel.toml``: The configuration file for the |Gel| project instance. Notice that we have a ``hooks.migration.apply.after`` hook that will run ``npx @gel/generate edgeql-js`` after migrations are applied. This will generate the query builder code that you'll use to interact with the database. More details on that to come! - ``dbschema/``: This directory contains the schema for the database, and later supporting files like migrations, and generated code. - :dotgel:`dbschema/default`: The default schema file that you'll use to define your data model. It is empty for now, but you'll add your data model to this file in the next step. - - ``lib/gel.ts``: A utility module that exports the Gel client, which you'll use to interact with the database. + - ``lib/gel.ts``: A utility module that exports the |Gel| client, which you'll use to interact with the database. .. tabs:: diff --git a/docs/intro/quickstart/working/fastapi.rst b/docs/intro/quickstart/working/fastapi.rst new file mode 100644 index 00000000000..ad120a6f46a --- /dev/null +++ b/docs/intro/quickstart/working/fastapi.rst @@ -0,0 +1,473 @@ +.. _ref_quickstart_fastapi_working: + +===================== +Working with the data +===================== + +In this section, you will update the existing FastAPI application to use |Gel| to store and query data, instead of a JSON file. Having a working application with mock data allows you to focus on learning how |Gel| works, without getting bogged down by the details of the application. + +Bulk importing of data +====================== + +.. edb:split-section:: + + First, update the imports and Pydantic models to use UUID instead of string for ID fields, since this is what |Gel| returns. You also need to initialize the |Gel| client and import the asyncio module to work with async functions. + + .. code-block:: python-diff + :caption: main.py + + from fastapi import FastAPI, HTTPException + from pydantic import BaseModel + from typing import List, Optional + - import json + - from pathlib import Path + + from uuid import UUID + + from gel import create_async_client + + import asyncio + + app = FastAPI(title="Flashcards API") + + # Pydantic models + class CardBase(BaseModel): + front: str + back: str + + class Card(CardBase): + - id: str + + id: UUID + + class DeckBase(BaseModel): + name: str + description: Optional[str] = None + + class DeckCreate(DeckBase): + cards: List[CardBase] + + class Deck(DeckBase): + - id: str + + id: UUID + cards: List[Card] + + - DATA_DIR = Path(__file__).parent / "data" + - DECKS_FILE = DATA_DIR / "decks.json" + + client = create_async_client() + + +.. edb:split-section:: + + Next, update the deck import operation to use |Gel| to create the deck and cards. The operation creates cards first, then creates a deck with links to the cards. Finally, it fetches the newly created deck with all required fields. + + .. note:: + + Notice the ``{ ** }`` in the query. This is a shorthand for selecting all fields of the object. It's useful when you want to return the entire object without specifying each field. In our case, we want to return the entire deck object with all the nested fields. + + .. code-block:: python-diff + :caption: main.py + + from fastapi import FastAPI, HTTPException + from pydantic import BaseModel + from typing import List, Optional + from uuid import UUID + from gel import create_async_client + import asyncio + + app = FastAPI(title="Flashcards API") + + # Pydantic models + class CardBase(BaseModel): + front: str + back: str + + class Card(CardBase): + id: UUID + + class DeckBase(BaseModel): + name: str + description: Optional[str] = None + + class DeckCreate(DeckBase): + cards: List[CardBase] + + class Deck(DeckBase): + id: UUID + cards: List[Card] + + client = create_client() + + - DATA_DIR.mkdir(exist_ok=True) + - if not DECKS_FILE.exists(): + - DECKS_FILE.write_text("[]") + + - def read_decks() -> List[Deck]: + - content = DECKS_FILE.read_text() + - data = json.loads(content) + - return [Deck(**deck) for deck in data] + - + - def write_decks(decks: List[Deck]) -> None: + - data = [deck.model_dump() for deck in decks] + - DECKS_FILE.write_text(json.dumps(data, indent=2)) + + @app.post("/decks/import", response_model=Deck) + async def import_deck(deck: DeckCreate): + - decks = read_decks() + - new_deck = Deck( + - id=str(uuid.uuid4()), + - name=deck.name, + - description=deck.description, + - cards=[Card(id=str(uuid.uuid4()), **card.model_dump()) + - for card in deck.cards] + - ) + - decks.append(new_deck) + - write_decks(decks) + - return new_deck + + card_ids = [] + + for i, card in enumerate(deck.cards): + + created_card = await client.query_single(""" + + insert Card { + + front := $front, + + back := $back, + + order := $order + + } + + """, front=card.front, back=card.back, order=i) + + card_ids.append(created_card.id) + + + + new_deck = await client.query_single(""" + + select( + + insert Deck { + + name := $name, + + description := $description, + + cards := ( + + select Card + + filter contains(>$card_ids, .id) + + ) + + } + + ) { ** } + + """, name=deck.name, description=deck.description, + + card_ids=card_ids) + + + + return new_deck + +.. edb:split-section:: + + The above works but isn't atomic - if any single query fails, you could end up with partial data. Let's wrap it in a transaction: + + .. code-block:: python-diff + :caption: main.py + + @app.post("/decks/import", response_model=Deck) + async def import_deck(deck: DeckCreate): + + async for tx in client.transaction(): + + async with tx: + card_ids = [] + for i, card in enumerate(deck.cards): + - created_card = await client.query_single( + + created_card = await tx.query_single( + """ + insert Card { + front := $front, + back := $back, + order := $order + } + """, + front=card.front, + back=card.back, + order=i, + ) + card_ids.append(created_card.id) + + - new_deck = await client.query_single(""" + + new_deck = await tx.query_single(""" + select( + insert Deck { + name := $name, + description := $description, + cards := ( + select Card + filter .id IN array_unpack(>$card_ids) + ) + } + ) { ** } + """, + name=deck.name, + description=deck.description, + card_ids=card_ids, + ) + + return new_deck + +.. edb:split-section:: + + One of the most powerful features of EdgeQL is the ability to compose complex queries in a way that is both readable and efficient. Use this super-power to create a single query that inserts the deck and cards, along with their links, in one efficient query. + + This new query uses a ``for`` expression to iterate over the set of cards, and sets the ``Deck.cards`` link to the result of inserting each card. This is logically equivalent to the previous approach, but is more efficient since it inserts the deck and cards in a single query. + + .. code-block:: python-diff + :caption: main.py + + @app.post("/decks/import", response_model=Deck) + async def import_deck(deck: DeckCreate): + - async for tx in client.transaction(): + - async with tx: + - card_ids = [] + - for i, card in enumerate(deck.cards): + - created_card = await tx.query_single( + - """ + - insert Card { + - front := $front, + - back := $back, + - order := $order + - } + - """, + - front=card.front, + - back=card.back, + - order=i, + - ) + - card_ids.append(created_card.id) + - + - new_deck = await client.query_single(""" + - select( + - insert Deck { + - name := $name, + - description := $description, + - cards := ( + - select Card + - filter .id IN array_unpack(>$card_ids) + - ) + - } + - ) { ** } + - """, + - name=deck.name, + - description=deck.description, + - card_ids=card_ids, + - ) + + cards_data = [(c.front, c.back, i) for i, c in enumerate(deck.cards)] + + + + new_deck = await client.query_single(""" + + select( + + with cards := >>$cards_data + + insert Deck { + + name := $name, + + description := $description, + + cards := ( + + for card in array_unpack(cards) + + insert Card { + + front := card.0, + + back := card.1, + + order := card.2 + + } + + ) + + } + + ) { ** } + + """, name=deck.name, description=deck.description, + + cards_data=cards_data) + + return new_deck + +Updating data +============= + +.. edb:split-section:: + + Next, update the deck operations. The update operation needs to handle partial updates of name and description: + + .. code-block:: python-diff + :caption: main.py + + @app.put("/decks/{deck_id}", response_model=Deck) + async def update_deck(deck_id: UUID, deck_update: DeckBase): + - decks = read_decks() + - deck = next((deck for deck in decks if deck.id == deck_id), None) + - if not deck: + - raise HTTPException(status_code=404, detail="Deck not found") + - + - deck.name = deck_update.name + - deck.description = deck_update.description + - write_decks(decks) + - return deck + + # Build update sets based on provided fields + + sets = [] + + params = {"id": deck_id} + + + + if deck_update.name is not None: + + sets.append("name := $name") + + params["name"] = deck_update.name + + + + if deck_update.description is not None: + + sets.append("description := $description") + + params["description"] = deck_update.description + + + + if not sets: + + return await get_deck(deck_id) + + + + updated_deck = await client.query(f""" + + with updated := ( + + update Deck + + filter .id = $id + + set {{ {', '.join(sets)} }} + + ) + + select updated { ** } + + """, **params) + + + + if not updated_deck: + + raise HTTPException(status_code=404, detail="Deck not found") + + + + return updated_deck + + +Adding linked data +================== + +.. edb:split-section:: + + Now, update the add card operation to use |Gel|. This operation will insert a new ``Card`` object and update the ``Deck.cards`` set to include the new ``Card`` object. Notice that the ``order`` property is set by selecting the maximum ``order`` property of this ``Deck.cards`` set and incrementing it by 1. + + The syntax for adding an object to a set of links is ``{ "+=": object }``. You can think of this as a shortcut for setting the link set to the current set plus the new object. + + .. code-block:: python-diff + :caption: main.py + + @app.post("/decks/{deck_id}/cards", response_model=Card) + async def add_card(deck_id: UUID, card: CardBase): + - decks = read_decks() + - deck = next((deck for deck in decks if deck.id == deck_id), None) + - if not deck: + - raise HTTPException(status_code=404, detail="Deck not found") + - + - new_card = Card(id=str(uuid.uuid4()), **card.model_dump()) + - deck.cards.append(new_card) + - write_decks(decks) + - return new_card + + new_card = await client.query_single( + + """ + + with + + deck := (select Deck filter .id = $id), + + order := (max(deck.cards.order) + 1), + + new_card := ( + + insert Card { + + front := $front, + + back := $back, + + order := order, + + } + + ), + + updated := ( + + update deck + + set { + + cards += new_card + + } + + ), + + select new_card { ** } + + """, + + id=deck_id, + + front=card.front, + + back=card.back, + + ) + + + + if not new_card: + + raise HTTPException(status_code=404, detail="Deck not found") + + + + return new_card + + +Deleting linked data +==================== + +.. edb:split-section:: + + As the next step, update the card deletion operation to use |Gel| to remove a card from a deck: + + .. code-block:: python-diff + :caption: main.py + + @app.delete("/cards/{card_id}") + async def delete_card(card_id: str): + - decks = read_decks() + - deck = next((deck for deck in decks if deck.id == deck_id), None) + - if not deck: + - raise HTTPException(status_code=404, detail="Deck not found") + - + - deck.cards = [card for card in deck.cards if card.id != card_id] + - write_decks(decks) + + deleted = await client.query_single(""" + + delete Card filter .id = $card_id + + """, card_id=card_id) + + + + if not deleted: + + raise HTTPException(status_code=404, detail="Card not found") + + + return {"message": "Card deleted"} + +Querying data +============= + +.. edb:split-section:: + + Finally, update the query endpoints to fetch data from |Gel|: + + .. code-block:: python-diff + :caption: main.py + + @app.get("/decks", response_model=List[Deck]) + async def get_decks(): + - return read_decks() + + decks = await client.query(""" + + select Deck { + + id, + + name, + + description, + + cards := ( + + select .cards { + + id, + + front, + + back + + } + + order by .order + + ) + + } + + """) + + return decks + + @app.get("/decks/{deck_id}", response_model=Deck) + async def get_deck(deck_id: UUID): + - decks = read_decks() + - deck = next((deck for deck in decks if deck.id == deck_id), None) + - if not deck: + - raise HTTPException(status_code=404, detail=f"Deck with id {deck_id} not found") + - return deck + + deck = await client.query_single(""" + + select Deck { + + id, + + name, + + description, + + cards := ( + + select .cards { + + id, + + front, + + back + + } + + order by .order + + ) + + } + + filter .id = $id + + """, id=deck_id) + + + + if not deck: + + raise HTTPException( + + status_code=404, + + detail=f"Deck with id {deck_id} not found" + + ) + + + + return deck + +.. edb:split-section:: + + You can now run your FastAPI application with: + + .. code-block:: sh + + $ uvicorn main:app --reload + +.. edb:split-section:: + + The API documentation will be available at http://localhost:8000/docs. You can use this interface to test your endpoints and import the sample flashcard deck. + + .. image:: images/flashcards-api.png diff --git a/docs/intro/quickstart/working/images/flashcards-api.png b/docs/intro/quickstart/working/images/flashcards-api.png new file mode 100644 index 0000000000000000000000000000000000000000..40cffeb5ea672e2cd9c7cd918776f0e08536b3b4 GIT binary patch literal 32396 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 literal 0 HcmV?d00001 diff --git a/docs/intro/quickstart/working/index.rst b/docs/intro/quickstart/working/index.rst index 8ab5d52a37a..bf8e51a8031 100644 --- a/docs/intro/quickstart/working/index.rst +++ b/docs/intro/quickstart/working/index.rst @@ -1,14 +1,12 @@ .. edb:env-switcher:: -======= -Working -======= +===================== +Working with the data +===================== .. toctree:: :maxdepth: 3 :hidden: nextjs - - - + fastapi