gel-js

repository·master·Indexed 20 days ago

https://github.com/geldata/gel-js

The official Node.js client library for Gel, providing high-performance database access for JavaScript and TypeScript applications. The library includes specialized packages such as @gel/ai for Retrieval-Augmented Generation (RAG) and vector embeddings, @gel/auth-core for low-level authentication utilities, and framework-specific authentication integrations including @gel/auth-express, @gel/auth-nextjs, @gel/auth-remix, and @gel/auth-sveltekit.

Tokens
74.1K
Snippets
250
Records
298
Agent score
66%

What's inside gel-js

  1. Overview of @gel/auth-core

    master

    The @gel/auth-core library provides low-level utilities for interacting with the Gel auth extension. Its primary responsibilities include:

    • Resolving API endpoint URLs from a Gel Client object.
    • Providing API wrappers to manage various authentication flows, including PKCE (Proof Key for Code Exchange).
    • Adding type safety to authentication operations.

    Note: This is a low-level utility library. For easier integration with specific web frameworks, it is recommended to use the dedicated helper libraries instead (e.g., @gel/auth-nextjs for Next.js).

  2. Build third-party generators with gel utilities

    master

    The gel package provides utilities to introspect the database schema and analyze queries, which can be used to build custom code generators. You can use $.introspect.types(client) to get a map of types and $.analyzeQuery(client, query) to analyze EdgeQL queries.

    Note: These utilities are accessed via the $ export from the gel package.

    import { createClient, $ } from "gel";
    
    const client = createClient();
    
    // Get a Map<string, Type> of all types in the schema
    const types = await $.introspect.types(client);
    
    // Analyze an EdgeQL query
    const queryData = await $.analyzeQuery(client, `select 2 + 2`);
  3. How portable shapes work with e.shape

    master

    You can define reusable shapes using e.shape. This returns a function that accepts a scope variable (the element being selected). When using a portable shape in a query, you must pass the scope variable into the shape function to allow the query builder to resolve the fields correctly.

    // Define the shape independently
    const baseShape = e.shape(e.Movie, (m) => ({
      title: true,
      num_actors: e.count(m)
    }));
    
    // Use the shape in a query by passing the scope variable 'm'
    const query = e.select(e.Movie, m => ({
      ...baseShape(m),
      release_year: true,
      filter_single: {title: 'The Avengers'}
    }));
  4. Use complex types as parameters

    master

    While EdgeQL parameters are typically primitives or arrays of primitives, the gel-js query builder supports arbitrarily complex parameter types. The query builder handles this by serializing the parameters to JSON and deserializing them on the server. This allows you to pass objects, tuples, and nested arrays directly into your query.

    const insertMovie = e.params(
      {
        title: e.str,
        release_year: e.int64,
        actors: e.array(
          e.tuple({
            name: e.str,
          })
        ),
      },
      (params) =>
        e.insert(e.Movie, {
          title: params.title,
        })
    );
    
    await insertMovie.run(client, {
      title: 'Dune',
      release_year: 2021,
      actors: [{name: 'Timmy'}, {name: 'JMo'}],
    });
  5. How EdgeDB enums are represented in TypeScript

    master

    The interfaces generator does not create TypeScript enum constructs. Instead, EdgeDB enum types are represented as unions of string literals.

    This approach is used because TypeScript enums are nominally typed (making two identical enums unequal) and involve runtime overhead, whereas string literal unions are purely static type-level constructs.

    Example Mapping:

    If your EdgeDB schema has:

    scalar type Genre extending enum<Horror, Comedy, Drama>;

    The generated TypeScript will be:

    export type Genre = "Horror" | "Comedy" | "Drama";
  6. Handle conflicts in nested bulk inserts

    master

    When performing bulk inserts where nested items (e.g., movies in a character insert) might conflict with each other within the same batch, a simple .unlessConflict on the main insert is insufficient because it only handles conflicts with data existing before the query started.

    To handle intra-query conflicts:

    1. Extract and De-duplicate: Use e.op("distinct", ...) on the nested items to ensure you only attempt to insert unique values.
    2. Use e.with: Break the nested insertion into its own top-level query block using e.with. This allows you to perform the de-duplication and insertion of the nested items first, then reference the resulting set when inserting the parent items.
    3. Use unlessConflict on the inner insert: This handles conflicts with data already in the database.
    const query = e.params(
      {
        characters: e.array(
          e.tuple({
            portrayed_by: e.str,
            name: e.str,
            movies: e.array(e.str),
          })
        ),
      },
      (params) => {
        // 1. Create a de-duplicated set of movies to insert first
        const movies = e.for(
          e.op(
            "distinct",
            e.array_unpack(e.array_unpack(params.characters).movies)
          ),
          (movieTitle) => {
            return e
              .insert(e.Movie, { title: movieTitle })
              .unlessConflict((movie) => ({
                on: movie.title,
                else: movie,
              }));
          }
        );
    
        // 2. Use e.with to scope the movie insertion at the top level
        return e.with(
          [movies],
          e.for(e.array_unpack(params.characters), (character) => {
            return e.insert(e.Character, {
              name: character.name,
              portrayed_by: character.portrayed_by,
              movies: e.assert_distinct(
                e.select(movies, (movie) => ({
                  filter: e.op(movie.title, "in", e.array_unpack(character.movies)),
                }))
              ),
            });
          })
        );
      }
    );
  7. Use the EdgeDB Query Builder for typed EdgeQL

    master

    The EdgeDB query builder provides a code-first, fully-typed way to write EdgeQL queries using TypeScript. It offers type inference for query results, autocompletion for EdgeQL keywords and schema names, and type checking to prevent invalid queries. Unlike a traditional ORM, it provides access to the full power of EdgeQL without performance tradeoffs, as queries are compiled into highly-optimized SQL by EdgeDB.

    import * as edgedb from "edgedb";
    import e from "./dbschema/edgeql-js";
    
    const client = edgedb.createClient();
    
    async function run() {
      const query = e.select(e.Movie, ()=>({
        id: true,
        title: true,
        actors: { name: true }
      }));
    
      const result = await query.run(client);
      /*
        result type is inferred as:
        { id: string; title: string; actors: { name: string; }[]; }[]
      */
    }
    
    run();
  8. Access object types in the query builder

    master

    All object types defined in your schema are reflected in the query builder. They are namespaced by their module. For the default module, types are available both within the e.default namespace and at the top-level of the client instance e for convenience.

    Type names serve two purposes:

    1. Representing the set of all objects of that type (e.g., select e.Movie).
    2. Representing the type itself for operations like type intersections (e.g., select e.Content[is e.Movie]).
    // Namespaced access
    e.default.Person;
    e.default.Movie;
    e.my_module.SomeType;
    
    // Top-level convenience access (for 'default' module)
    e.Person;
    e.Movie;
    e.TVShow;
  9. Write polymorphic queries using types

    master

    Types are used to implement polymorphism in queries, typically via the e.is function within an e.select block. This allows you to conditionally select fields based on the specific subtype of an object.

    e.select(e.Content, content => ({
      title: true,
      ...e.is(e.Movie, { release_year: true }),
      ...e.is(e.TVShow, { num_seasons: true }),
    }));
  10. e.for vs JavaScript loops (Performance and Atomicity)

    master

    When iterating over data to perform database operations, prefer e.for over JavaScript's for or .forEach methods.

    Featuree.for (Server-side)JS Loops (Client-side)
    PerformanceHigh (Single query)Low (Multiple queries)
    AtomicityGuaranteed (Single atomic query)Requires manual transaction management
    Data IntegrityAll succeed or all failRisk of partial success/failure

    Note: For extremely large datasets, it may be practical to batch queries and run them individually to manage memory/load, but e.for is the standard for most use cases.

  11. How automatic WITH blocks work

    master

    The query rendering engine automatically optimizes queries by tracking expression occurrences. If an expression is used more than once within a query, it is automatically extracted into a with block to avoid redundant computation. This applies to expressions of any complexity, including nested inserts or operations.

    const x = e.int64(3);
    const y = e.select(e.op(x, '^', x));
    
    y.toEdgeQL();
    // with x := 3
    // select x ^ x
    
    const result = await y.run(client);
    // => 27
  12. Access properties in EdgeDB Objects and Sets

    master

    An EdgeDB Object is returned as a JavaScript object where properties and links are accessible via keys.

    Sets: When an object property is a multi set, it is returned as an edgedb.Set. An edgedb.Set is array-like (has .length and index access) but is a distinct type from a standard JavaScript Array.

    const assert = require("assert");
    const edgedb = require("edgedb");
    
    async function main() {
      const client = edgedb.createClient("edgedb://edgedb@localhost/");
    
      const data = await client.querySingle(`
        select schema::Property {
            name,
            annotations: {name, @value}
        }
        filter .name = 'listen_port'
            and .source.name = 'cfg::Config'
        limit 1
      `);
    
      // Property access
      assert(typeof data.name === "string");
      
      // Set access (links)
      assert(data.annotations instanceof edgedb.Set);
      assert(data.annotations.length > 0);
      assert(data.annotations[0].name === "cfg::system");
    }
    
    main();