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.

  1. const query = e.select(e.Movie, ()=>({
  2. id: true,
  3. title: true,
  4. actors: { name: true }
  5. }));
  6. const result = await query.run(client)
  7. // { id: string; title: string; actors: {name: string}[] }[]

Why use the query builder?

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.

Requirements

The query builder works for both JavaScript and TypeScript users, and supports both Node.js and Deno. It’s possible to use the query builder with or without TypeScript. Some requirements apply to TypeScript users only.

  • Node: v14+. Run node --version to see your current version.

  • Deno: v0.20+.

  • TypeScript: v4.4+.

    • Node users: install Node.js types with npm install @types/node.

    • Make sure the following compilerOptions exist in your tsconfig.json

  1. ```
  2. // tsconfig.json
  3. {
  4. // ...
  5. "compilerOptions": {
  6. // ...
  7. "strict": true,
  8. "downlevelIteration": true,
  9. }
  10. }
  11. ```

Quickstart

If you haven’t already, initialize a project, write your 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:

  1. type Movie {
  2. required property title -> str { constraint exclusive; };
  3. property release_year -> int64;
  4. multi link actors -> Person;
  5. }
  6. type Person {
  7. required property name -> str;
  8. }

Generate the query builder

With Node.js, use npx to generate the query builder.

  1. $
  1. npx edgeql-js

With Deno, use deno run. Deno user must also create an import map, see “Deno usage” below.

  1. $
  1. deno run https://deno.land/x/edgedb/generate.ts

This 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.

The first time you generate the query builder you’ll be prompted to add the generated files to your .gitignore. Confirm this prompt to add the the line automatically.

  1. $
  1. npx edgeql-js
  1. ...
  2. Checking the generated query builder into version control
  3. is NOT RECOMMENDED. Would you like to update .gitignore to ignore
  4. the query builder directory? The following line will be added:
  5. dbschema/edgeql-js
  6. [y/n] (leave blank for "y")

Deno usage

The query builder generates code that depends on the edgedb module. The generated code imports using a plain module name (import {createClient} from "edgedb"). You must configure an import map to tell Deno how to resolve this import.

In your deno.json

  1. {
  2. // ...
  3. "importMap": "./import_map.json"
  4. }

Then create import_map.json with the following contents. Both lines must be present.

  1. {
  2. "imports": {
  3. "edgedb": "https://deno.land/x/edgedb/mod.ts",
  4. "edgedb/": "https://deno.land/x/edgedb/"
  5. }
  6. }

Optionally, you can specify a version:

  1. {
  2. "imports": {
  3. "edgedb": "https://deno.land/x/edgedb@v0.22.0/mod.ts",
  4. "edgedb/": "https://deno.land/x/edgedb@v0.22.0/"
  5. }
  6. }

Import the query builder

Create a TypeScript file called script.ts (the name doesn’t matter) and import the query builder. We recommend importing the query builder as a single default import called e.

  1. // script.ts
  2. import e from "./dbschema/edgeql-js";

Create a client

The query builder is only used to write queries, not execute them. To execute queries, we still need a client that manages the actual connection to our EdgeDB instance. See the Driver docs for full details.

  1. // script.ts
  2. import {createClient} from "edgedb";
  3. import e from "./dbschema/edgeql-js";
  4. const client = createClient();

If you’ve initialized a project, there’s no need to provide connection information to createClient; it will connect to your project-linked instance by default. You can override this by setting the value of the EDGEDB_DSN environment variable; refer to the Connection docs for more information.

Write a query

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

  1. // script.ts
  2. import {createClient} from "edgedb";
  3. import e from "./dbschema/edgeql-js";
  4. const client = createClient();
  5. async function run() {
  6. const query = e.select(e.datetime_current());
  7. const result = await query.run(client);
  8. console.log(result);
  9. }
  10. 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 as its first input.

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

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

Executing expressions

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.

  1. import * as edgedb from "edgedb";
  2. const client = edgedb.createClient();
  3. await e.str("hello world").run(client);
  4. // => "hello world"
  5. e.set(e.int64(1), e.int64(2), e.int64(3));
  6. // => [1, 2, 3]
  7. e.select(e.Movie, ()=>({
  8. title: true,
  9. actors: { name: true }
  10. }));
  11. // => [{ 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.

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

Converting to EdgeQL

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.)

  1. e.str("hello world");
  2. // => select "hello world"
  3. e.set(e.int64(1), e.int64(2), e.int64(3));
  4. // => select {1, 2, 3}
  5. e.select(e.Movie, ()=>({
  6. title: true,
  7. actors: { name: true }
  8. }));
  9. // => select Movie { title, actors: { name }}

Extracting the inferred type

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.

  1. import e, {$infer} from "./dbschema/edgeql-js";
  2. const query = e.select(e.Movie, () => ({ id: true, title: true }));
  3. type result = $infer<typeof query>;
  4. // {id: string; title: string}[]

Cheatsheet

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.

Insert an object

  1. const query = e.insert(e.Movie, {
  2. title: 'Doctor Strange 2',
  3. release_year: 2022
  4. });
  5. const result = await query.run(client);
  6. // {id: string}
  7. // by default INSERT only returns
  8. // the id of the new object

Select objects

  1. const query = e.select(e.Movie, () => ({
  2. id: true,
  3. title: true,
  4. }));
  5. const result = await query.run(client);
  6. // Array<{id: string; title: string}>

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

  1. const query = e.select(e.Movie, () => ({
  2. ...e.Movie['*']
  3. }));
  4. const result = await query.run(client);
  5. /* Array{
  6. id: string;
  7. title: string;
  8. release_year: number | null; # optional property
  9. }> */

Nested shapes

  1. const query = e.select(e.Movie, () => ({
  2. id: true,
  3. title: true,
  4. actors: {
  5. name: true,
  6. }
  7. }));
  8. const result = await query.run(client);
  9. // Array<{id: string; title: string, actors: Array<{name: string}>}>

Filtering, ordering, and pagination

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

  1. const query = e.select(e.Movie, (movie) => ({
  2. id: true,
  3. title: true,
  4. filter: e.op(movie.release_year, ">", 1999),
  5. order_by: movie.title,
  6. limit: 10,
  7. offset: 10
  8. }));
  9. const result = await query.run(client);
  10. // Array<{id: string; title: number}>

Operators

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

  1. // prefix (unary) operators
  2. e.op('not', e.bool(true)); // not true
  3. e.op('exists', e.set('hi')); // exists {'hi'}
  4. // infix (binary) operators
  5. e.op(e.int64(2), '+', e.int64(2)); // 2 + 2
  6. e.op(e.str('Hello '), '++', e.str('World!')); // 'Hello ' ++ 'World!'
  7. // ternary operator (if/else)
  8. e.op(e.str('😄'), 'if', e.bool(true), 'else', e.str('😢'));
  9. // '😄' if true else '😢'

Select a single object

Filter by a property with an exclusive constraint to fetch a particular object.

  1. const query = e.select(e.Movie, (movie) => ({
  2. id: true,
  3. title: true,
  4. release_year: true,
  5. // filter .id = '2053a8b4-49b1-437a-84c8-e1b0291ccd9f'
  6. filter: e.op(movie.id, '=', '2053a8b4-49b1-437a-84c8-e1b0291ccd9f'),
  7. }));
  8. const result = await query.run(client);
  9. // {id: string; title: string; release_year: number | null}

Note that result is now a single object, not a list of objects. The query builder detects when you are filtering on a property with an exclusive constraint.

Update objects

  1. const query = e.update(e.Movie, (movie) => ({
  2. filter: e.op(movie.title, '=', 'Doctor Strange 2'),
  3. set: {
  4. title: 'Doctor Strange in the Multiverse of Madness',
  5. },
  6. }));
  7. const result = await query.run(client);

Delete objects

  1. const query = e.delete(e.Movie, (movie) => ({
  2. filter: e.op(movie.title, 'ilike', "the avengers%"),
  3. }));
  4. const result = await query.run(client);
  5. // Array<{id: string}>

Compose queries

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.

  1. const newMovie = e.insert(e.Movie, {
  2. title: "Iron Man",
  3. release_year: 2008
  4. });
  5. const query = e.select(newMovie, ()=>({
  6. title: true,
  7. release_year: true,
  8. num_actors: e.count(newMovie.actors)
  9. }));
  10. const result = await query.run(client);
  11. // {title: string; release_year: number; num_actors: number}

Or we can use subqueries inside mutations.

  1. // select Doctor Strange
  2. const drStrange = e.select(e.Movie, movie => ({
  3. filter: e.op(movie.title, '=', "Doctor Strange")
  4. }));
  5. // select actors
  6. const actors = e.select(e.Person, person => ({
  7. filter: e.op(person.name, 'in', e.set('Benedict Cumberbatch', 'Rachel McAdams'))
  8. }));
  9. // add actors to cast of drStrange
  10. const query = e.update(drStrange, ()=>({
  11. actors: { "+=": actors }
  12. }));

Query parameters

  1. const query = e.params({
  2. title: e.str,
  3. release_year: e.int64,
  4. },
  5. ($) => {
  6. return e.insert(e.Movie, {
  7. title: $.title,
  8. release_year: $.release_year,
  9. }))
  10. };
  11. const result = await query.run(client, {
  12. title: 'Thor: Love and Thunder',
  13. release_year: 2022,
  14. });
  15. // {id: string}

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

Globals

Reference global variables.

  1. e.global.user_id;
  2. e.default.global.user_id; // same as above
  3. e.my_module.global.some_value;