Gelly reference

Gelly is the data access language powering advanced features in Gadget like Computed fields. Gelly is a superset of GraphQL following the same principles of declarative queries that return predictable results.

All Gelly code must be wrapped in a stanza defining it's purpose. Like GraphQL or SQL, Gelly isn't a statement based language executed one line at a time. Instead, a Gelly snippet declares one whole unit of work that is executed all at once by the underlying platform. The available stanzas are Queries, Fragments, and Field Fragments.

Queries

A query in Gelly declares a bunch of expressions and returns them. Gelly queries are very similar to GraphQL queries or SQL SELECT statements -- they submit what data they want and then the platform returns it all in a predictable shape. A Gelly query can select fields from the underlying models, expressions on those fields to manipulate or aggregate them, as well as control which records from the models are returned.

As of June 2021, there's currently no way to execute a raw Gelly query in Gadget, but that will change soon.

Selecting fields of models

Queries can select fields from the available models by listing each field between curly brackets (a selection set) from the plural name of that model. For example, this query selects the id and title field from the first page of a Blog Post model:

gelly
1query {
2 posts {
3 id
4 title
5 }
6}

This query will return a JSON document containing the selected fields for each blog post that looks like this:

json
1{
2 "posts": [
3 { "id": 1, "title": "Hello World" },
4 { "id": 2, "title": "Thoughts on Software Development" }
5 ]
6}

As many fields as needed can be added to the selection set, and a query can select from more than one model at once if needed. This query selects the first page of Blog Post ids, titles and publish dates, as well as the current user's name and email:

gelly
1query {
2 posts {
3 id
4 title
5 publishedDate
6 }
7 currentUser {
8 name
9 email
10 }
11}

The underlying Gelly implementation will efficiently query these two models in parallel and return the data for both as a JSON object like this:

json
1{
2 "posts": [{"id": 1, ...}, {"id": 2, ...}],
3 "currentUser": {
4 "name": "Jane McGee",
5 "email": "[email protected]"
6 }
7}

Fields in a selection must be separated by at least one whitespace character like a space, a tab, or a newline. Model fields are selected using the API Identifier for that field, not the field name.

Which fields you select is entirely up to you -- none are mandatory, and none require other fields to be selected in order to be selected themselves. Usually, the requirements for which fields are being selected are driven by the specific product experience you're trying to build. It's generally faster to select only the fields you need for a specific screen or component in an application, so developers will write one Gelly query (or fragment) per screen and only execute it when that screen is visited.

Selecting relationships

Gelly allows related data for each record to be selected along side the containing record. To selected related data, add an inner set of curly brackets (a nested selection set) to a root selection.

For example, this query selects the id and title of the first page of blog posts, as well as the id and commentBody of first page of comments for each of the posts:

gelly
1query {
2 posts {
3 id
4 title
5 comments {
6 id
7 commentBody
8 }
9 }
10}

This query returns a JSON document where each element of the posts array will have a comments key holding another array of objects with the fields for each comment:

json
1{
2 "posts": [
3 {
4 "id": 1,
5 "title": "Hello World",
6 "comments": [
7 {
8 "id": 1,
9 "commentBody": "Nice post!"
10 },
11 {
12 "id": 2,
13 "commentBody": "Cool blog!"
14 }
15 ]
16 }
17 ]
18}

Relationship fields can be deeply nested, so you can select relationships of relationships. For example, if Blog Post HasMany Comments, and then Comments HasMany Comment Upvotes, you can select all three models at the same time like so:

gelly
1query {
2 posts {
3 id
4 title
5 comments {
6 id
7 commentBody
8 commentUpvotes {
9 id
10 createdAt
11 }
12 }
13 }
14}

Expressions

Gelly allows selecting arbitrary expressions of fields in order to ask the server to transform records before returning results to the client. Transformations can be simple string operations or math, complicated expressions with boolean logic, or aggregations that summarize a whole set of records into one value.

For example, we can select the full name for each record of a Customer model by adding the firstName and lastName fields together.

gelly
1query {
2 customers {
3 id
4 fullName: firstName + " " + lastName
5 }
6}

This query would produce a result like the one on the right.

json
{
"customers": [{ "fullName": "Jane McGee" }, { "fullName": "Joe Schmoe" }]
}

Expressions are selected right alongside other fields, and must be separated from each other with whitespace characters. Expressions can refer to other fields of the record in question, as well as use literals like strings and numbers to do useful work. Expressions can be simple literals, which return value the literal's value.

gelly
query {
1
}

json
{
"1": 1
}

Expressions can also be complicated, and call functions or use boolean logic.

gelly
1query {
2 comments {
3 id
4 isSpam: author.markedAsSpammer || contains(lower(title), "spam") || author.spamScore < (endsWith(author.email, "@gmail.com") ? 10 : 5)
5 }
6}

Aliasing fields and expressions

You can name an expression (or rename a field) when selecting it by prefixing the selection with a name and a colon. For example, to select the title field of a Blog Post model but have the resulting JSON blob use the key humanName to store the field's value, you can run this query:

gelly
query {
posts {
humanName: title
}
}

This would produce a JSON result like:

json
{
"posts": [{ "humanName": "Hello World!" }, { "humanName": "Thoughts on Software Development" }]
}

Aliasing works on expressions as well. To select an upper-case'd version of the title field from the Blog Post model outputted as the screamingTitle key, the expression can be aliased by prefixing the selection with an alias identifier and a colon.

gelly
query {
posts {
screamingTitle: upcase(title)
}
}

This would produce a JSON result like the one on the right.

json
{
"posts": [{ "screamingTitle": "HELLO WORLD!" }, { "humanName": "THOUGHTS ON SOFTWARE DEVELOPMENT" }]
}

If you don't alias an expression, the expression's source Gelly snippet is used as the output field name. For example, querying for upcase(title) will return the uppercase title under a field named upcase(title)

gelly
query {
posts {
upcase(title)
}
}

This would produce a JSON result like the one on the right.

json
{
"posts": [{ "upcase(title)": "HELLO WORLD!" }, { "upcase(title)": "THOUGHTS ON SOFTWARE DEVELOPMENT" }]
}

Ternary expressions

Gelly supports if-then-else statements using the ? ternary expression, similar to JavaScript. Put a boolean expression first followed by a ? symbol, and then two expressions separated by a :. When the boolean expression is true, the first expression is selected, otherwise the second expression is selected.

For a simple example, we can select the string "yes" if 10 is greater than 5:

gelly
query {
isTenGreaterThanFive: 10 > 5 ? "yes" : "no"
}

Which would produce a result JSON like so:

json
{
"isTenGreaterThanFive": "yes"
}

Ternary expressions can also be used within selections of relations alongside other selections just fine.

gelly
1query {
2 posts {
3 id
4 title
5 recent: createdAt < (now() - interval("60 days")) ? createdAt : "Old"
6 }
7}

Unlike SQL, ternary expressions don't need to return the same data type from the truthy and falsy expressions on either side of the colon.

Commands

Which records are passed through a selection sets in Gelly (the stuff between the {}) can be changed using relational commands like where, limit, order by and group by. Relational commands are always enclosed in square brackets within a selection set, and can occur anywhere inside it.

gelly
1query {
2 posts {
3 id
4 title
5 [limit 5]
6 }
7}

Relational commands change the data returned by the query, but don't change the data stored in the database. Relational commands are able to use expressions that use data from the model being queried. For example, if the Blog Post model had a published boolean field on it, then we can use the where relational command to filter the list of returned posts down to only those that are published.

gelly
1query {
2 posts {
3 id
4 title
5 [where published]
6 }
7}

We can also use expressions to filter returned records. For example, we could return only posts with a score above a certain threshold that are also published:

gelly
1query {
2 posts {
3 id
4 title
5 [where published && score > 10]
6 }
7}

Relational commands can be used within inner, nested selection sets without affecting the outer selection set. For example, we can select the top three comments for each post sorted by comment score:

gelly
1query {
2 posts {
3 id
4 title
5 comments {
6 id
7 body
8 [order by score desc limit 3]
9 }
10 }
11}

We can also issue multiple relational commands to inner and outer selection sets simultaneously. For example, we can select the top three comments for each post, and only select posts-with-comments for posts that are published, and only return the most recent 3 posts:

gelly
1query {
2 posts {
3 id
4 title
5 [where published limit 3 order by createdDate desc]
6
7 comments {
8 id
9 body
10 [order by score desc limit 3]
11 }
12 }
13}

The order of relational commands within the [...] stanza doesn't matter, and the position of the command list among all the fields in a selection set also doesn't matter.

Relational commands are very similar (and in fact powered by) the SQL query clauses of the same name, and tend to work the same way as their SQL counterparts. If you are familiar with SQL, you can use the relational commands like you would use the same keywords in SQL!

The limit command

The limit relational command ensures a maximum number of results are returned for the selection set. A selection set containing [limit 10] will return a maximum of 10 records. The limit command must be passed one fixed integer, and can't use dynamic expressions to choose how many rows are returned. If the underlying model has fewer records than the limit, or if a where command filters them out, up to the limit records will be returned.

[limit <number>]

For example, we could select up to of 5 posts with the limit command.

Limiting to a count
gelly
1query {
2 posts {
3 title
4 [limit 5]
5 }
6 }

The limit command can be used more than once, on both an outer selection and an inner selection. For example, we could select 5 posts and 3 comments for each post with two limit commands.

Limiting at different nesting
gelly
1query {
2 posts {
3 title
4 [limit 5]
5 comments {
6 body
7 [limit 3]
8 }
9 }
10 }

The limit command is often combined with the order by command in order to get the top records by some criteria. For example, we can get the top 10 scoring posts ever by combining the two commands.

Limiting at different nesting
gelly
1query {
2 posts {
3 title
4 score
5 [order by score desc limit 10]
6 }
7 }

The order by command

The order by relational command sorts the returned records by the specified expression in either ascending or descending order. The default sort direction is ascending, where the record with the lowest value will be first, and the record with the highest value will be last. Most, but not all data types that can be ordered by.

[order by <expression> <asc|desc>]

For example, order by can ensure the most recent records for a table are returned by using [order by createdDate desc].

Ordering by a date field
gelly
1query {
2 posts {
3 title
4 [order by createdDate desc]
5 }
6 }

The expression can a rich computation on any available data instead of just a simple field reference.

Ordering by an expression
gelly
1query {
2 posts {
3 title
4 [order by author.scoreWeight * post.score]
5 }
6 }

The where command

The where command filters the stored records down to only return some of them. where expects to be passed a boolean expression and will include records where the expression evaluates to true.

[where <expression>]

For example, you can find only the posts that have a score greater than 0 with the where command within the posts selection set:

Positive score posts
gelly
1query {
2 posts {
3 title
4 [where score > 0]
5 }
6 }

where command expressions can be arbitrarily complicated. For example, you can query for posts that have a score over 50 by a trusted author, or posts which have a score over 50 by a spammy author.

Interesting posts
gelly
1query {
2 posts {
3 title
4 score
5 author.isSpam
6 [where (!author.isSpam && score > 50) || (author.isSpam && score > 100)]
7 }
8 }

Like all relational commands, the where command can be combined with other relational commands. For example, we can select the top 10 highest scoring posts where the author isn't spammy by using where, limit, and order by.

Interesting posts
gelly
1query {
2 posts {
3 title
4 score
5 [where !author.isSpam order by score desc limit 10]
6 }
7 }

The group by command

The group by command allows aggregating individual, smaller buckets of the total records available to return one aggregate result per group. group by can be used to answer questions like "the top score by city" or "the count of comments made each day for the last 30 days". [group by] in Gelly is very similar to GROUP BY in SQL.

[group by <expression> <, ...expression> <, ...expression>]

group by expects to given the expressions to group the incoming records using. The expression (or expressions) are evaluated for each row in the set being grouped, and then a group is formed for each unique value (or combination of values) for the expressions. Any aggregate functions in the selection set are then evaluated independently for each group instead of across the whole incoming set of records. For example, we could group post records by their author id to count the number of posts each author has made. This query would return one row per unique authorId value holding the authorId and the count(id) counting the number of posts. Be sure to explicitly select the authorId field in order to know which count is for which authorId in the results.

Post count by author
gelly
1query {
2 posts {
3 count(id)
4 authorId
5 [group by authorId]
6 }
7 }

group by in Gelly tends to be used less than in SQL. There's nothing wrong with using it, but in Gelly, there's often a simpler way of executing the same query without using group by that most consider clearer, specifically when grouping the related records of a parent record. If you're grouping a set of records by the same key of a relationship, you can use a sub selection, or an inline function call instead!

The same query above using group by authorId can instead be written to use an inline aggregation function. This switches the response to be oriented around the authors model, which allows for getting things like the author's name field easily, instead of just the counts.

Authors with count of posts
gelly
1query {
2 authors {
3 id
4 name
5 count(posts.id)
6 }
7 }

group by can group by arbitrary expressions on fields as well. We could count the number of posts by a score bucket, where we use the modulus operator to round each post score to a multiple of 10. Note that we want to select the scoreBucket expression so we can associate which count is for which bucket in the results.

Post count by score bucket
gelly
1query {
2 posts {
3 count(id)
4 scoreBucket: score % 10
5 [group by scoreBucket]
6 }
7 }

Like all relational commands, group by can be combined with other related commands. limit and order by affect the group results, and queries are able to order by fields produced by the grouping. For example, we could order the post-counts-by-author results by the resulting post count.

Ordering aggregate results
gelly
1query {
2 posts {
3 postCount: count(id)
4 authorId
5 [group by authorId order by postCount desc]
6 }
7 }

group by can be combined with where commands as well. Gelly groups records after they have been filtered by a where command. This allows you to group only records you are interested in. For example, we can compute the count of posts by author where the post was not marked as a spam.

Ham Post count by author
gelly
1query {
2 posts {
3 count(id)
4 authorId
5 [
6 group by authorId
7 where !isSpam
8 ]
9 }
10 }

All selected expressions must aggregate the input rows when grouped

Like GROUP BY in SQL, the group by command will error if you try to select fields that aren't part of the group expressions. This is because the output rows from a group by must be aggregated, and Gelly doesn't know what to do with fields that you don't specify an aggregate function for. For example, if we group posts by authorId, and then try to select the count of posts, and each post's score, it is unclear what to do with the score of each post to form the group. To fix this issue, use an aggregate function on each field you want to select which defines how to process the field for the group, or switch the orientation of your query to select from an outer relation where you can select anything you want, and then group a different inner relation.

Ungrouped fields cause errors
gelly
1query {
2 posts {
3 count(id)
4 # will cause an error because the field is not being aggregated
5 score
6 [group by authorId]
7 }
8 }

Fragments

There's often a need to re-use specific parts of Gelly queries in different places, in the same way you might re-use a JavaScript function as part of a larger piece of code, or re-use part of an HTML template in different outer contexts. Gelly supports fragments to facilitate composition and re-use among queries. The Gadget platform also uses fragments to allow Gadget developers to co-operate with the automatic schema generation and extend it. Fragments in Gelly work similarly to GraphQL, but don't have an exact analog in SQL.

Fragments can only be declared at the top level, and are declared using the fragment keyword. They are defined with a name that is then used to refer to them later, followed by the type of the object they are selecting fields from. Fragments can contain field selections, nested field selections, and relational commands. For example, we could use a fragment to specify which fields want to select from the posts model. We use the fragment keyword to start a fragment definition, give it a name, and then use the same field selection syntax you might use in a query.

gelly
1fragment PostDetails on Post {
2 id
3 title
4}
5
6query {
7 posts {
8 ...PostDetails
9 [where published]
10 }
11}

Queries can apply fragments using the spread operator, which is the .... Spreading a fragment can be thought of as copy-pasting the fragment's source code into the location of the spread.

In order to support re-usability of relational commands too, fragments support embedded relational commands. When spread, these fragments apply those commands to the query, merging the query's commands with their own. For example, we could have a PublishedPostDetails fragment that we re-use in several queries that selects some fields as well as filters the posts that are returned. Queries that spread fragments can add more filters, or add other relational commands, and still select other fields.

Fragments with relational commands
gelly
1fragment PublishedPostDetails on Post {
2 id
3 title
4 [where published]
5 }
6 query {
7 posts {
8 ...PostDetails
9 author {
10 id
11 }
12 [limit 10]
13 }
14 }

Gelly doesn't allow both a fragment and a query that spreads it to both specify a group by relational command or an order by relational command. Only the fragment or the query can use these commands, but not both.

Fragment variables

Gelly fragments can accept arguments and then be spread with different values more than once. Variables can be used within the fragment to change behavior, and are really useful for re-using a particular fragment more than once with slightly different specifics in each place. Fragment variables are also used for configuring Gadget's permissions system with custom logic that can change behaviour based on the $currentUser variable.

Fragment variables are defined the same way as query variables using parentheses after the name of the fragment definition. Fragments can then be spread with different values for these variables in any context where a fragment would normally be valid.

Fragments with variables
gelly
1fragment PostsForAuthor($authorId: ID!) on Post {
2 id
3 title
4 [where authorId = $authorId]
5 }
6 query {
7 posts {
8 ...PostsForAuthor(authorId: "123")
9 author {
10 id
11 }
12 [limit 10]
13 }
14 }

Fragments can take many variables, and can use the variables within field selections or relational commands without issue. Fragment variables can also be passed values from outer query variables or arbitrary expressions.

Fragments with variables
gelly
1fragment PostDetails($onlyInteresting: Boolean) on Post {
2 hotRightNow: $onlyInteresting ? score > 100 : false
3 [where $onlyInteresting ? score > 10 : true]
4 }
5 query($full: Boolean) {
6 posts {
7 id
8 title
9 ...PostDetails(onlyInteresting: $full)
10 [limit 10]
11 }
12 }

Fragment variables in Gelly don't work the same way as variables work in GraphQL. Variables in Gelly are similar to arguments in JavaScript or Python where they must be declared at the top of a fragment or query, and only the variables that are declared are accessible for the duration of that fragment or query. If you want to pass a value from a query stanza into a fragment stanza, you must declare that the fragment accepts that variable, and pass it in when you spread the fragment.

Fragments with variables
gelly
1# the $onlyInteresting variable won't be available in this fragment unless we declare it at the top of the fragment
2 fragment PostDetails($onlyInteresting: Boolean) on Post {
3 hotRightNow: $onlyInteresting ? score > 100 : false
4 [where $onlyInteresting ? score > 10 : true]
5 }
6 query($onlyInteresting: Boolean) {
7 posts {
8 id
9 title
10 # the $onlyInteresting variable must be passed into the fragment, even if it has the same name in the query context
11 ...PostDetails(onlyInteresting: $onlyInteresting)
12 [limit 10]
13 }
14 }

Field fragments

Gelly has a second fragment syntax for fragments which define one new field on a model. These are called field fragments, and are a Gelly-only concept that doesn't really have an analog in GraphQL or SQL. Field fragments are fragments which return a single value, so they are different than normal fragments because they can't select multiple fields from the input. Field fragments are used when implementing a Computed field in Gadget. You can read more about Computed in the Gelly Guide.

Field fragments are specified using the field keyword, followed by the name of the field being defined, followed by the type of the model the field is being defined on. Inside the curly braces Gelly expects to find exactly one expression. For example, we could define a field on posts called isPopular with a field fragment.

isPopular field fragment
gelly
1# in the Gadget editor, in isPopular.gelly
2 field isPopular on Post {
3 score > 100
4 }
5 # in a Gelly query later
6 query {
7 posts {
8 title
9 isPopular
10 [limit 10]
11 }
12 }

Field fragments can specify arbitrary expressions, including useful stuff like aggregates on relationships of the model being queries. For example, we could use a field fragment on a Computed to power a new field of the Author model which counts the number of popular posts that author has ever written.

popularPostCount field fragment
gelly
1# in the Gadget editor, in popularPostCount.gelly
2 field popularPostCount on Author {
3 count(posts, where: posts.score > 10)
4 }
5 # in a Gelly query later
6 query {
7 authors {
8 id
9 name
10 popularPostCount
11 [limit 10]
12 }
13 }

Field fragments aren't able to use relational commands.

Built-in data types

Gelly supports literals for a variety of built in data types that you can use in your expressions. The syntax is very similar to JavaScript, so you can usually use the same syntax to declare literals as you might in JS within Gelly snippets.

Integers and Numbers are written out as the digits of the number:

Number literals
gelly
query {
anInteger: 5
aDecimal: 10.5
}

Booleans are written as true or false:

Boolean literals
gelly
query {
yes: true
no: false
}

Strings are written with the characters between double quotes, and using \" to escape quote characters within the string:

String literals
gelly
query {
aString: "foo"
withAQuoteInIt: "when she said "yes", it was incredible!"
}

DateTimes are written using the timestamp function which accepts a timestamp string as input, or the date function which accepts a date string as input. The date string is formatted according to Postgres' timestamp and date literal syntax, which is documented in the Postgres docs. If not specified, times are assumed to be in UTC.

Date literals
gelly
query {
aTimestamp: timestamp("2021-05-01 15:00:00")
anISO8601Date: timestamp("2021-05-01T15:00:00Z")
aDate: date("2021-05-01")
}

nulls are written as null. There's no undefined in Gelly.

Arrays are written between square brackets with commas (,) separating the elements.

Array literals
gelly
query {
someNumbers: [1, 2, 3]
someStrings: ["foo", "bar", "baz"]
myStuff: [1, "foo", true, null]
}

Objects are written between curly brackets with the keys on the left hand side of a colon (:) and commas (,) separating the elements.

Object literals
gelly
query {
anObject: {foo: "bar", baz: true}
}

Built-in operators

Gelly has a number of built in operators for arithmetic, comparison, and boolean logic.

Boolean logic

Prefix a boolean expression with an exclamation mark to inverse the boolean value.

Logical not
gelly
query {
!false # returns true
}

Test if two booleans are both true with &&, or if either boolean is true with ||

The number line
gelly
1query {
2 true && true # returns true
3 true && false # returns false
4 true || false # returns true
5 false || false # returns false
6 }

Equality and inequality

Test if two expressions are equal with two equals signs, like ==.

Expression Equality
gelly
1query {
2 1 == 1 # returns true
3 1 == 2 # returns false
4 true == true # returns true
5 true == false # returns false
6 }

Test if two expressions are not equal with !=.

Expression Inequality
gelly
1query {
2 1 != 1 # returns false
3 1 != 2 # returns true
4 true != true # returns false
5 true != false # returns true
6 }

Numeric ordering

Test how two numbers relate to each other with >, >=, <, and <=.

The number line
gelly
query {
1 > 1 # returns false
1 >= 1 # returns true
10 > 1 # returns true
}

Arithmetic

Add, subtract, multiply or divide numbers with +, -, *, or /:

Back to elementary school
gelly
query {
1 + 1 # returns 2
2 * 10 # returns 10
10 / 2 # returns 5
}

Division in Gelly returns decimal numbers with arbitrary precision. There is no integer division operator. This is different than most SQL engines, which do integer division by default, and ECMAScript, which uses IEEE-754 floating point numbers and thus has limited precision for mathematics.

Division
gelly
query {
10 / 3 # returns 3.3 repeating
}

Use the % operator for a modulus (integer division remainder).

Modulus
gelly
query {
10 % 3 # returns 1
}

Boolean functions

isNull(<value:> Any): Boolean!

Returns true if the passed value is null, and false otherwise.

To check if something is null, you must use the isNull function, and not do an equality check like == null. This is because like SQL, null in Gelly is a special state of a data type that exists for every data type, and not a value itself. This different than JavaScript or other statement based languages where null == null, in Gelly, anything == null returns null itself. This is called three-valued logic and allows folks writing Gelly queries to not have to do constant null checks to make sure that a value is not null before manipulating or selecting it.

Null checks
gelly
query {
isNull(null) # returns true
isNull({apple: "red"}.apple) # returns false
isNull({apple: "red"}.orange) # returns true
}

String functions

concat(<string:> [String!]!, delimiter: String): String!

Returns a new string built by stringing each input string together, optionally with a delimiter.

Concatenate string literals together
gelly
query {
concat(["Hello", "", "world!"])
}
Concatenate fields from a selection together
gelly
query {
posts {
details: concat([title, " by ", author.name])
}
}
Concatenate array with a delimiter
gelly
query {
posts {
details: concat(tags, delimiter: ", ")
}
}

leftSlice(<string:> String!, length: Number): String!

Returns a substring of the input string, starting from the first (left most) character position and including length characters after.

leftSlice a string
gelly
query {
leftSlice("foobar", length: 3) # returns "foo"
}

length(<string:> String!): Integer!

Returns the number of characters in the passed string.

get the length of a string
gelly
query {
length("foobar") # returns 6
}

lower(<string:> String!): String!

Returns the lower-cased version of the input string.

lowercase a string
gelly
query {
lower("FooBar") # returns "foobar"
}

rightSlice(<string:> String!, length: Number): String!

Returns a substring of the input string, starting from the last (right most) character position and including length previous characters.

Rightslice a string
gelly
query {
rightSlice("foobar", length: 3) # returns "bar"
}

slice(<string:> String!, start: Number = 0, length: Number): String!

Returns a substring of the input string, starting from the start character position, or the first character if no start is passed, and going length characters further, or to the end of the string if length is not passed.

Slice a string from the start
gelly
query {
slice("foobar", length: 3) # returns "foo"
}

The start character position is inclusive, so start: 0 would include the first character in the string onwards, or start: 1 would skip the first character but include the second character onwards.

Slice a string in the middle
gelly
query {
slice("foobar", start: 2, length: 3) # returns "oba"
}

If the start argument is provided and the length argument isn't provided, the remainder of the string from the start position is returned.

Slice a string in the middle
gelly
query {
slice("foobar", start: 4) # returns "ar"
}

upper(<string:> String!): String!

Returns the upper-cased version of the input string.

uppercase a string
gelly
query {
upper("FooBar") # returns "FOOBAR"
}

Aggregation functions

The where argument

All aggregate functions in Gelly take a where argument which allows for filtering the set of records that specific function applies to. The where argument doesn't filter the returned set of records, or affect other function calls, it only affects the aggregate function to which it is passed. where is a convenient way of quickly filtering records without having to filter the whole record set, but can be used interchangeably with the where relational command.

Using where with the count function allows counting records which match a certain criteria.

Count the number of published posts
gelly
query {
posts {
count(id, where: isPublished)
}
}

This is equivalent to running count with a [where] relational command.

Count the number of published posts
gelly
1query {
2 posts {
3 count(id)
4 [where isPublished]
5 }
6 }

Notably, the where argument can be used to issue multiple aggregate calls at once with different filter conditions without having to do many outer selection sets. The [where] relational command filters all records before passing them onto the functions, so all aggregate functions in a selection set are given the same, already filtered list of records to aggregate.

Aggregate posts with different filters
gelly
1query {
2 posts {
3 totalCount: count(id)
4 publishedCount: count(id, where: isPublished)
5 highScoreCount: count(id, where: score > 10)
6 bannedCount: count(id, where: author.banned)
7 }
8 }

avg(<number:> Number!, where: Boolean): Number!

Averages the given field number across the aggregated set, optionally filtered to only consider values which pass the where condition.

Average all post scores by author
gelly
1query {
2 posts {
3 avg(score)
4 [group by author.name]
5 }
6 }

To do a filtered average easily, you can use the where argument to the avg function.

Average only published post scores
gelly
query {
posts {
avg(score, where: isPublished)
}
}

`every(where: Boolean): Boolean!

Returns true if all the passed values are true themselves, or false if any of the passed values are falsy.

Check if all posts have been published
gelly
query {
posts {
every(published)
}
}

To check if a subset of records match the passed conditions, use a where relational command.

Check if all published posts have a high score
gelly
1query {
2 posts {
3 every(score > 10)
4 [where published]
5 }
6 }

count(<value:> Any, where: Boolean): Integer!

Counts the number of non-null values across the aggregated set, optionally filtered to only consider values which pass the where condition.

Count the number of posts
gelly
query {
posts {
count(id)
}
}

To do a filtered count easily, you can use the where argument to the count function.

Count the number of published posts
gelly
query {
posts {
count(id, where: isPublished)
}
}

count is also subject to the relational commands for the query, so you can filter a count aggregate as well as other aggregates at the same time.

Get the average score and count of published posts
gelly
1query {
2 posts {
3 count(id)
4 avg(score)
5 [where published]
6 }
7 }

max(<number:> Number!, where: Boolean): Number!

Finds the maximum value of the number argument across the aggregated set, optionally filtered to only consider values which pass the where condition.

Find the maximum post scores by author
gelly
1query {
2 posts {
3 max(score)
4 [group by author.name]
5 }
6 }

To find the maximum value in a subset of records easily, you can use the where argument to the max function.

Maximum published post score
gelly
query {
posts {
max(score, where: isPublished)
}
}

min(<number:> Number!, where: Boolean): Number!

Finds the minimum value of the number argument across the aggregated set, optionally filtered to only consider values which pass the where condition.

Find the minimum post scores by author
gelly
1query {
2 posts {
3 min(score)
4 [group by author.name]
5 }
6 }

To find the minimum value in a subset of records easily, you can use the where argument to the min function.

Minimum published post score
gelly
query {
posts {
min(score, where: isPublished)
}
}

sum(<number:> Number!, where: Boolean): Number!

Adds the value of the number argument together across the whole aggregated set, optionally adding only values which pass the where condition.

Get the total votes cast by author
gelly
1query {
2 posts {
3 sum(voteCount)
4 [group by author.name]
5 }
6 }

To sum only the value from a subset of records easily, you can use the where argument to the sum function.

Maximum published post score
gelly
query {
posts {
max(score, where: isPublished)
}
}

Time functions

date(input: String!): DateTime!

Create a DateTime object from an input string representing a date (without a time). Uses the Postgres date and time formatting syntax -- see more details in the Postgres docs.

Create a DateTime object
gelly
query {
date("2021-05-05")
}
Posts scheduled for publishing after a certain threshold
gelly
1query {
2 posts {
3 id
4 publishAt
5 [where publishAt > date("2021-05-05")]
6 }
7 }

datePart(part: String!, date: DateTime!): Number!

Retrieve a given component of a DateTime object, like the year, week, or minutes. Uses the Postgres syntax for the date part, and returns it as a number. The valid parts are century, day, decade, dow (day of week), doy (day of year), epoch, hour, isodow, isoyear, microseconds, millennium, milliseconds, minute, month, quarter, second, week, year.

See more details about date parts in the Postgres docs.

Get the year of a date
gelly
query {
datePart("year", date("2021-05-05"))
}
Day of week publishing schedule
gelly
1query {
2 posts {
3 id
4 publishDay: datePart("dow", publishAt)
5 }
6 }

interval(input: String!): Interval!

Create a Interval object from an input string representing a specific duration of time. Intervals are useful for doing math with dates. interval uses the Postgres interval formatting syntax -- see more details in the Postgres docs.

Create an Interval value
gelly
query {
short: interval("5 minutes")
long: interval("1 year")
longer: interval("1 year") + interval("6 months")
}
Posts published in the last 60 days
gelly
1query {
2 posts {
3 id
4 publishAt
5 [where publishAt > now() - interval("60 days")]
6 }
7 }

now(): DateTime!

Gets the current system time from the Gelly server.

Current Time
gelly
query {
now()
}
Posts scheduled for publishing in the future
gelly
1query {
2 posts {
3 id
4 publishAt
5 [where publishAt > now()]
6 }
7 }
Posts scheduled for publishing in the future
gelly
1query {
2 posts {
3 id
4 publishAt
5 [where publishAt > now()]
6 }
7 }

timestamp(input: String!): DateTime!

Create a DateTime object from an input string representing a specific point in time. Uses the Postgres time formatting syntax -- see more details in the Postgres docs. If not specified, times are assumed to be in UTC.

Create a DateTime value
gelly
query {
timestamp("2021-05-05 10:10:00")
}
Posts scheduled for publishing after a certain threshold
gelly
1query {
2 posts {
3 id
4 publishAt
5 [where publishAt > timestamp("2021-05-01T15:00:00Z")]
6 }
7 }

Numeric functions

abs(<number:> Number!): Integer!

Gets the absolute value of a given input number.

Absolute value
gelly
query {
abs(-10) # returns 10
abs(10) # returns 10
}

ceil(<number:> Number!): Integer!

Returns the nearest integer greater than or equal to the input number.

Ceiling numbers
gelly
1query {
2 ceil(2.5) # returns 3
3 ceil(3) # returns 3
4 ceil(-2.5) # returns -2
5 ceil(-1) # returns -1
6 }

floor(<number:> Number!): Integer!

Returns the nearest integer less than or equal to the input number.

Flooring numbers
gelly
1query {
2 floor(2.5) # returns 2
3 floor(3) # returns 3
4 floor(-2.5) # returns -3
5 floor(-1) # returns -1
6 }

exp(<number:> Number!): Number!

Returns the exponential (the constant e raised to the given power) of the input number.

Exponential of numbers
gelly
query {
exp(1) # returns 2.7182818284590452
}

ln(<number:> Number!): Number!

Returns the natural logarithm (the logarithm with base e) of the input number.

Natural Logarithm of numbers
gelly
query {
ln(1) # returns 0
ln(10) # returns 2.302585092994046
}

log(<number:> Number!, base: Number = 10): Number!

Returns the logarithm with base base of the input number. The base defaults to 10.

Natural Logarithm of numbers
gelly
query {
log(10) # returns 1
log(10, base: 2) # returns 0.1505149978319906
}

mod

There is no mod function in Gelly. Instead, use the % operator.

power(<number:> Number!, exponent: Number!): Number!

Returns the given number raised to the given exponent.

Natural Logarithm of numbers
gelly
query {
power(10, 3) # returns 100
power(2, 5) # returns 32
}

round(<number:> Number!, precision: Number = 0): Number!

Rounds the given fractional number to the nearest number of decimal places, counted by precision. Precision defaults to 0, so when not passed round rounds to an integer number.

Rounding numbers
gelly
1query {
2 round(1) # returns 1
3 round(1.11111) # returns 1
4 round(1, precision: 2) # returns 1
5 round(1.11111, precision: 2) # returns 1.11
6 }

sign(<number:> Number!): Number!

Returns 1 if the number is positive, -1 if the number is negative, or 0 if the number is 0.

sqrt(<number:> Number!): Number!

Returns the square root of the given number.

Number square roots
gelly
query {
sqrt(4) # returns 2
sqrt(10) # returns 3.162277660168379
}

trunc(<number:> Number!, precision: Number = 0): Number!

Truncates the given fractional number to the number of decimal places counted by precision. trunc doesn't do any rounding, precision is simply discarded, so if you want to round the number, see round. precision defaults to 0.

Number square roots
gelly
1query {
2 trunc(1) # returns 1
3 trunc(1.11111) # returns 1
4 trunc(1.111111, precision: 2 # returns 1.11
5 trunc(1, precision: 2) # returns 1
6 }

random(): Number!

Returns a random number greater than or equal to 0 and less than 1.

Trigonometric functions and constants

acos(<number:> Number!): Number!

Returns the inverse cosine of the input number given in radians, output in radians.

asin(<number:> Number!): Number!

Returns the inverse sine of the input number given in radians, output in radians.

atan(<number:> Number!): Number!

Returns the inverse tangent of the input number given in radians, output in radians.

cos(<number:> Number!): Number!

Returns the cosine of the input number given in radians, output in radians.

cot(<number:> Number!): Number!

Returns the cotangent of the input number given in radians, output in radians.

degrees(<radians:> Number!): Number!

Converts the given input radians in radians to degrees.

pi

Returns the value of pi, accurate to 15 decimal places.

radians(<degrees:> Number!): Number!

Converts the given input degrees in degrees to radians.

sin(<number:> Number!): Number!

Returns the sine of the input number given in radians, output in radians.

tan(<number:> Number!): Number!

Returns the tangent of the input number given in radians, output in radians.

GraphQL differences

Gelly is similar to and inspired by GraphQL -- it's a really good idea. There are some key differences between the two systems thought:

  • Gelly queries can contain arbitrary expressions which are evaluated on the server side

This is so that folks on the front end can push as much work to the server as possible, and so that they don't need to manually add new, bespoke API endpoints for each separate thing they want to do. Sometimes it is simpler to just ask the server to upcase a string, or filter on an expression, or group by an arbitrary field that the user has selected. So, Gelly has math, function calls, aggregations, and relational commands built in.

  • Gelly has relational commands available on every relation instead of some GraphQL fields having arguments

In Gelly, the relational commands [where], [order by], [limit] and [group by] are universally available on every relation. This allows developers to work quickly and leverage the automatic query compilation and execution that Gadget does for each Gadget application. Some GraphQL APIs expose similar facilities to Gelly's relational commands, but as bespoke field arguments, and that often require backend work to set up and make performant. Aggregations in Gelly aren't exposed as other strange fields in a schema, and are instead available as aggregate functions

  • Gelly doesn't have Relay style Connections, and instead has a formal idea of a relation

Gelly is a syntax around a fundamentally very powerful concept called the relational algebra. Just like in the algebra, relations in Gelly are things you can manipulate, and so we don't work with them as just more types in the system, and instead bake the idea in deeply. There's no edges { node { ... }} selections in Gelly, there's just subselections of relations. We have big plans for enhancing Gelly with support for the full relational algebra including joins as well as a relational chaining operator for implementing subqueries.

  • The Gelly schema can be extended with Gelly itself, which allows for great performance

Sometimes, it is valuable to teach the server about a new field that should be present on the model and work the same way for all clients, which Gelly supports with Field Fragments and Computed. These schema extensions are defined in the Gelly language itself, which allows the platform to push that work into high performance database queries, instead of shuffling a lot of data to the client for client side computation.

  • Gelly doesn't support Mutations

GraphQL supports writes and Gelly does not (yet). If you're building a Gadget app, you can use the GraphQL API to execute writes to your application.

  • Gelly doesn't support Subscriptions or @live queries

Some GraphQL servers support subscribing to a specific field, and some even support asking for arbitrary changes to a query result over time, both in a push style. Gelly doesn't yet support these facilities.

  • Gelly null works like SQL NULL, not like JavaScript's null, where null == null gives null.

Gelly is powered by SQL underneath the hood, and so inherits SQL's null semantics. This means that null == null returns null in Gelly, not true. To check if something is null, you should use the isNull function.