Search
ctrl/
Ask AI
Light
Dark
System

Query Builder Generator

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
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)
  /*
    {
      id: string;
      title: string;
      actors: { name: string; }[];
    }[]
  */
}

run();

Is it an ORM?

No—it’s better! Like any modern TypeScript ORM, the query builder gives you full typesafety and autocompletion, but without the power and performance tradeoffs. You have access to the full power of EdgeQL and can write EdgeQL queries of arbitrary complexity. And since EdgeDB compiles each EdgeQL query into a single, highly-optimized SQL query, your queries stay fast, even when they’re complex.

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.

Close to EdgeQL! The goal of the query builder is to provide an API that is as close as possible to EdgeQL itself while feeling like idiomatic TypeScript.

To get started, install the following packages.

If you’re using Deno, you can skip this step.

Install the edgedb package.

Copy
$ 
npm install edgedb       # npm users
Copy
$ 
yarn add edgedb          # yarn users
Copy
$ 
bun add edgedb           # bun users

Then install @edgedb/generate as a dev dependency.

Copy
$ 
npm install @edgedb/generate --save-dev      # npm users
Copy
$ 
yarn add @edgedb/generate --dev              # yarn users
Copy
$ 
bun add --dev @edgedb/generate               # bun users

The following command will run the edgeql-js query builder generator.

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

Deno users

Create these two files in your project root:

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

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.

--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
await e.str("hello world").run(client);
// => "hello world"

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

await e
  .select(e.Movie, () => ({
    title: true,
    actors: { name: true },
  }))
  .run(client);
// => [{ 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").toEdgeQL();
// => select "hello world"

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

e.select(e.Movie, () => ({
  title: true,
  actors: { name: true }
})).toEdgeQL();
// => 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 type helper.

Copy
import e, { type $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

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

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

await client.transaction(async (tx) => {
  const result = await query.run(tx);
  // { id: string }
});
Copy
const query = e.select(e.Movie, () => ({
  id: true,
  title: true,
}));

const result = await query.run(client);
// { 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);
/*
  {
    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);
/*
  {
    id: string;
    title: string;
    actors: { 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);
// { 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: e.op(
    movie.id,
    "=",
    e.uuid("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 when used on exclusive properties:

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);
// { id: string }[]

Delete multiple objects using an array of properties:

Copy
const titles = ["The Avengers", "Doctor Strange 2"];
const query = e.delete(e.Movie, (movie) => ({
  filter: e.op(
    movie.title,
    "in",
    e.array_unpack(e.literal(e.array(e.str), titles))
  )
}));
const result = await query.run(client);
// { id: string }[]

Note that we have to use array_unpack to cast our array<str> into a set<str> since the in operator works on sets. And we use literal to create a custom literal since we’re inlining the titles array into our query.

Here’s an example of how to do this with params:

Copy
const query = e.params({ titles: e.array(e.str) }, ({ titles }) =>
  e.delete(e.Movie, (movie) => ({
    filter: e.op(movie.title, "in", e.array_unpack(titles)),
  }))
);

const result = await query.run(client, {
  titles: ["The Avengers", "Doctor Strange 2"],
});
// { 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 }
}));

const result = await query.run(client);
Copy
const query = e.params({
  title: e.str,
  release_year: e.int64,
},
(params) => {
  return e.insert(e.Movie, {
    title: params.title,
    release_year: params.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;

Reference entities in modules other than default.

The Vampire type in a module named characters:

Copy
e.characters.Vampire;

As shown in “Globals,” a global some_value in a module my_module:

Copy
e.my_module.global.some_value;