Light
Dark
System

Query Builder

The EdgeDB query builder provides a code-first way to write fully-typed EdgeQL queries with TypeScript. We recommend it for TypeScript users, or anyone who prefers writing queries with code.

Copy
const query = e.select(e.Movie, ()=>({
  id: true,
  title: true,
  actors: { name: true }
}));

const result = await query.run(client)
// { id: string; title: string; actors: {name: string}[] }[]

Type inference! If you’re using TypeScript, the result type of all queries is automatically inferred for you. For the first time, you don’t need an ORM to write strongly typed queries.

Auto-completion! You can write queries full autocompletion on EdgeQL keywords, standard library functions, and link/property names.

Type checking! In the vast majority of cases, the query builder won’t let you construct invalid queries. This eliminates an entire class of bugs and helps you write valid queries the first time.

If you haven’t already, initialize a project, write a schema, and create/ apply a migration. Follow the Quickstart for a guided walkthrough of this process.

The rest of this walkthrough uses the following simple Movie schema:

Copy
type Movie {
  required property title -> str { constraint exclusive; };
  property release_year -> int64;
  multi link actors -> Person;
}

type Person {
  required property name -> str;
}

Run the following command to generate the query builder.

Node.js
Deno
Copy
$ 
npx @edgedb/generate edgeql-js
Copy
$ 
deno run --allow-all --unstable https://deno.land/x/edgedb/generate.ts edgeql-js

The generator detects whether you’re using TypeScript or JavaScript and generates the appropriate files into the dbschema/edgeql-js directory. Refer to the Targets section to learn how to customize this.

If you’re seeing a connection error or another issue, refer to the Generation docs for more complete documentation, then return to this tutorial.

Usage with Deno

The query builder generates code that depends on the edgedb module. The generated code uses Node-style import paths (import {createClient} from "edgedb"). For Deno to resolve these properly, you must configure an import map. In your deno.json

deno.json
importMap.json
Copy
{
  // ...
  "importMap": "./importMap.json"
}
Copy
{
  "imports": {
    "edgedb": "https://deno.land/x/edgedb/mod.ts",
    "edgedb/": "https://deno.land/x/edgedb/"
  }
}

The first time you run the generator, you’ll be prompted to add the generated files to your .gitignore. Confirm this prompt to automatically add a line to your .gitignore that excludes the generated files.

Copy
$ 
npx @edgedb/generate edgeql-js
...
Checking the generated query builder into version control
is not recommended. Would you like to update .gitignore to ignore
the query builder directory? The following line will be added:

   dbschema/edgeql-js

[y/n] (leave blank for "y")

For consistency, we recommend omitting the generated files from version control and re-generating them as part of your deployment process. However, there may be circumstances where checking the generated files into version control is desirable, e.g. if you are building Docker images that must contain the full source code of your application.

Once the query builder is generated, it’s ready to use! We recommend importing the query builder as a single default import called e.

Copy
// Node.js + TypeScript
import e from "./dbschema/edgeql-js";

// TypeScript with ESM
import e from "./dbschema/edgeql-js/index.mjs";

// JavaScript (ES modules)
import e from "./dbschema/edgeql-js/index.mjs";

// Deno
import e from "./dbschema/edgeql-js/index.ts";

// JavaScript (CommonJS)
const e = require("./dbschema/edgeql-js");

If you’re using ES modules, remember that imports require a file extension. The rest of the documentation uses Node.js + TypeScript syntax.

Now we have everything we need to write and execute our first query!

Copy
// script.ts
import {createClient} from "edgedb";
import e from "./dbschema/edgeql-js";

const client = createClient();

async function run() {
  const query = e.select(e.datetime_current());
  const result = await query.run(client);
  console.log(result);
}
run();

We use the e object to construct queries. The goal of the query builder is to provide an API that is as close as possible to EdgeQL itself. So select datetime_current() becomes e.select(e.datetime_current()). This query is then executed with the .run() method which accepts a client or a transaction as its first input.

Run that script with the tsx like so. It should print the current timestamp (as computed by the database).

Copy
$ 
npx tsx script.ts
2022-05-10T03:11:27.205Z

We can also run the same query as above, built with the query builder, in a transaction.

Copy
const query = e.select(e.datetime_current());
client.transaction(async tx => {
  const result = await query.run(tx);
  console.log(result);
});

The generation command is configurable in a number of ways.

--output-dir <path>

Sets the output directory for the generated files.

--target <ts|cjs|esm|mts>

What type of files to generate. Documented above.

--force-overwrite

To avoid accidental changes, you’ll be prompted to confirm whenever the --target has changed from the previous run. To avoid this prompt, pass --force-overwrite.

The generator also supports all the connection flags supported by the EdgeDB CLI. These aren’t necessary when using a project or environment variables to configure a connection.

Throughout the documentation, we use the term “expression” a lot. This is a catch-all term that refers to any query or query fragment you define with the query builder. They all conform to an interface called Expression with some common functionality.

Most importantly, any expression can be executed with the .run() method, which accepts a Client instead as the first argument. The result is Promise<T>, where T is the inferred type of the query.

Copy
import * as edgedb from "edgedb";

const client = edgedb.createClient();

await e.str("hello world").run(client);
// => "hello world"

e.set(e.int64(1), e.int64(2), e.int64(3));
// => [1, 2, 3]

e.select(e.Movie, ()=>({
  title: true,
  actors: { name: true }
}));
// => [{ title: "The Avengers", actors: [...]}]

Note that the .run method accepts an instance of Client() (or Transaction) as it’s first argument. See Creating a Client for details on creating clients. The second argument is for passing $parameters, more on that later.

Copy
.run(client: Client | Transaction, params: Params): Promise<T>

You can extract an EdgeQL representation of any expression calling the .toEdgeQL() method. Below is a number of expressions and the EdgeQL they produce. (The actual EdgeQL the create may look slightly different, but it’s equivalent.)

Copy
e.str("hello world");
// => select "hello world"

e.set(e.int64(1), e.int64(2), e.int64(3));
// => select {1, 2, 3}

e.select(e.Movie, ()=>({
  title: true,
  actors: { name: true }
}));
// => select Movie { title, actors: { name }}

The query builder automatically infers the TypeScript type that best represents the result of a given expression. This inferred type can be extracted with the $infer helper.

Copy
import e, {$infer} from "./dbschema/edgeql-js";

const query = e.select(e.Movie, () => ({ id: true, title: true }));
type result = $infer<typeof query>;
// {id: string; title: string}[]

Below is a set of examples to get you started with the query builder. It is not intended to be comprehensive, but it should provide a good starting point.

Modify the examples below to fit your schema, paste them into script.ts, and execute them with the npx command from the previous section! Note how the signature of result changes as you modify the query.

Copy
const query = e.insert(e.Movie, {
  title: 'Doctor Strange 2',
  release_year: 2022
});

const result = await query.run(client);
// {id: string}
// by default INSERT only returns
// the id of the new object
Copy
const query = e.select(e.Movie, () => ({
  id: true,
  title: true,
}));

const result = await query.run(client);
// Array<{id: string; title: string}>

To select all properties of an object, use the spread operator with the special * property:

Copy
const query = e.select(e.Movie, () => ({
  ...e.Movie['*']
}));

const result = await query.run(client);
/* Array<{
  id: string;
  title: string;
  release_year: number | null;  # optional property
}> */
Copy
const query = e.select(e.Movie, () => ({
  id: true,
  title: true,
  actors: {
    name: true,
  }
}));

const result = await query.run(client);
// Array<{id: string; title: string, actors: Array<{name: string}>}>

Pass a boolean expression as the special key filter to filter the results.

Copy
const query = e.select(e.Movie, (movie) => ({
  id: true,
  title: true,
  // special "filter" key
  filter: e.op(movie.release_year, ">", 1999)
}));

const result = await query.run(client);
// Array<{id: string; title: number}>

Since filter is a reserved keyword in EdgeQL, the special filter key can live alongside your property keys without a risk of collision.

The e.op function is used to express EdgeQL operators. It is documented in more detail below and on the Functions and operators page.

To select a particular object, use the filter_single key. This tells the query builder to expect a singleton result.

Copy
const query = e.select(e.Movie, (movie) => ({
  id: true,
  title: true,
  release_year: true,

  filter_single: {id: '2053a8b4-49b1-437a-84c8-e1b0291ccd9f'},
}));

const result = await query.run(client);
// {id: string; title: string; release_year: number | null}

For convenience filter_single also supports a simplified syntax that eliminates the need for e.op:

Copy
e.select(e.Movie, (movie) => ({
  id: true,
  title: true,
  release_year: true,

  filter_single: {id: '2053a8b4-49b1-437a-84c8-e1b0291ccd9f'},
}));

This also works if an object type has a composite exclusive constraint:

Copy
/*
  type Movie {
    ...
    constraint exclusive on (.title, .release_year);
  }
*/

e.select(e.Movie, (movie) => ({
  title: true,
  filter_single: {title: 'The Avengers', release_year: 2012},
}));

The special keys order_by, limit, and offset correspond to equivalent EdgeQL clauses.

Copy
const query = e.select(e.Movie, (movie) => ({
  id: true,
  title: true,

  order_by: movie.title,
  limit: 10,
  offset: 10
}));

const result = await query.run(client);
// {id: true; title: true}[]

Note that the filter expression above uses e.op function, which is how to use operators like =, >=, ++, and and.

Copy
// prefix (unary) operators
e.op('not', e.bool(true));      // not true
e.op('exists', e.set('hi'));    // exists {'hi'}

// infix (binary) operators
e.op(e.int64(2), '+', e.int64(2)); // 2 + 2
e.op(e.str('Hello '), '++', e.str('World!')); // 'Hello ' ++ 'World!'

// ternary operator (if/else)
e.op(e.str('😄'), 'if', e.bool(true), 'else', e.str('😢'));
// '😄' if true else '😢'
Copy
const query = e.update(e.Movie, (movie) => ({
  filter_single: {title: 'Doctor Strange 2'},
  set: {
    title: 'Doctor Strange in the Multiverse of Madness',
  },
}));

const result = await query.run(client);
Copy
const query = e.delete(e.Movie, (movie) => ({
  filter: e.op(movie.title, 'ilike', "the avengers%"),
}));

const result = await query.run(client);
// Array<{id: string}>

All query expressions are fully composable; this is one of the major differentiators between this query builder and a typical ORM. For instance, we can select an insert query in order to fetch properties of the object we just inserted.

Copy
const newMovie = e.insert(e.Movie, {
  title: "Iron Man",
  release_year: 2008
});

const query = e.select(newMovie, ()=>({
  title: true,
  release_year: true,
  num_actors: e.count(newMovie.actors)
}));

const result = await query.run(client);
// {title: string; release_year: number; num_actors: number}

Or we can use subqueries inside mutations.

Copy
// select Doctor Strange
const drStrange = e.select(e.Movie, movie => ({
  filter_single: {title: "Doctor Strange"}
}));

// select actors
const actors = e.select(e.Person, person => ({
  filter: e.op(person.name, 'in', e.set('Benedict Cumberbatch', 'Rachel McAdams'))
}));

// add actors to cast of drStrange
const query = e.update(drStrange, ()=>({
  actors: { "+=": actors }
}));
Copy
const query = e.params({
  title: e.str,
  release_year: e.int64,
},
($) => {
  return e.insert(e.Movie, {
    title: $.title,
    release_year: $.release_year,
  }))
};

const result = await query.run(client, {
  title: 'Thor: Love and Thunder',
  release_year: 2022,
});
// {id: string}

Continue reading for more complete documentation on how to express any EdgeQL query with the query builder.

Reference global variables.

Copy
e.global.user_id;
e.default.global.user_id;  // same as above
e.my_module.global.some_value;
Light
Dark
System