edgedb / edgedb-js

The official TypeScript/JS client library and query builder for EdgeDB
https://edgedb.com
Apache License 2.0
510 stars 65 forks source link

Template tag for EdgeQL #350

Open colinhacks opened 2 years ago

colinhacks commented 2 years ago

Proposal: provide a template tag for writing sanitized EdgeQL queries.

User input

import {edgeql} from "edgedb";

const query = edgeql`insert User { title := ${req.body.title}}`
// => { query: 'insert User { title := <str>$var_0 }', parameters: {var_0: "Some title"}}

We'd overload the query methods to support an object-based query format: {query: string; parameters: {[k:string]: any}}

await client.query(query)

The cast type will be inferred from the input data according to the following rules:

Implicit/assignment casting makes this workable. e.g. Assigning a float64 to a int16 value will work without an explicit cast.

Identifiers

There should be a separate API for specifying identifiers: edgeql.ident(). This would verify that the input is a valid identifier with a regex.

import {edgeql, ident} from "edgedb";

const field1 = 'User'
const field2 = 'email'
edgeql`insert ${ident(field1)} { ${ident(field2)} };`

For convenience this function can be variadic. The inputs will be validated and joined with ", ".

edgeql`select User { ${ident('name', 'email')} }`
// select User { name, email }

### Prior art:

https://github.com/felixfbecker/node-sql-template-strings
https://github.com/blakeembrey/sql-template-tag
https://www.npmjs.com/package/pg-format
1st1 commented 2 years ago

For convenience this function can be variadic. The inputs will be validated and joined with ", ".

Probably it accepting an array of strings is better since I imagine sometimes such lists would be formed dynamically, and ident(list) is better than ident.apply(null, list).

colinhacks commented 2 years ago

Probably it accepting an array of strings is better

With a variadic function you can use the spread operator to pass in an array of identifiers: ident(...list). (This can be documented.)

elprans commented 2 years ago

This would verify that the input is a valid identifier with a regex.

Pretty much any Unicode string is a valid identifier as long as you quote it. The only hard rule is that an ident cannot start with $ or @.

jaclarke commented 2 years ago

The cast type will be inferred from the input data according to the following rules:

I think it's probably safe to add the rest of the types to this list, ie. datetime, local_date/time, duration and bytes, since we can identify them with instanceof. The only exception would then be uuid, and the user would just have manually add a cast.

colinhacks commented 2 years ago

I think it's probably safe to add the rest of the types to this list

Indeed good call!

infogulch commented 2 years ago

I see draft PR #381 implements this functionality. See also discussion on edgedb/edgedb.

This may be premature, but is there a way to expose the result type of a tagged template query to typescript for autocompletion / typechecking purposes? E.g. using .name on an entry of a select query that does not include a name field should be a type error.

colinhacks commented 2 years ago

No, this template tag will not be schema-aware nor can it infer types. That's virtually impossible to implement with just template literals - you should use the query builder if you want inferred types and typechecking.

haikyuu commented 2 years ago

That's virtually impossible to implement with just template literals

Indeed, but it would be possible with a VSCode extension. It would introspect each query at dev-time and tell typescript what type that is

infogulch commented 2 years ago

It would introspect each query at dev-time and tell typescript what type that is

I'm not sure how that would work exactly, but I've observed some existing solutions in the same space:

[^1]: At least until we get F#-style type providers

Option 1

One option is a codegen tool that would watch the source for queries tagged with edgeql<TResult>`select ...` and generate the correct generic TResult type for that query in a separate generated file just before building with tsc. The benefit of this approach is that you can implement automatically generated types without any changes to the standard typescript tooling. The downside is that you have pushed that complexity into the build system at the next layer up, by requiring you to run a different external program after every change to make sure types are in sync with queries. Another downside is that even though they don't have to keep the result type in sync with the query manually, the user still has to uniquely name and import each result type which is 'somewhat' annoying.

GraphQL libraries have taken this path in the past [^2] and seem to have converged on graphql-code-generator which is a centralized codegen integration point that alternate generators can plug into|plugin to.

Option 2

Out of graphql-code-generator, a promising approach is the gql-tag-operations babel plugin (source) which is "a babel plugin for rewriting the gql usages to actual imports". This might be the 'nicest' design I've seen because it minimizes user input down to typing the query and the source is automatically transformed on build into a fully-typed reference to a type in a different generated file. We may need to discuss how acceptable it is to require users to use babel so they can import a babel plugin, or find some other acceptable way to integrate what is basically preprocessing/macros into the typescript build system.


I prefer Option 2 for edgeql if possible. I expect that automatically adding types to edgeql`` queries by transforming the source during build would be very nice UX. Failing that, Option 1 would still be good.

I don't mean to derail this issue, and I think it would still be reasonable to implement the tagged template as described in the original post, and splitting this topic into a second issue. I also think that being able to generate typesafe code by simply tagging edgeql queries could be a big benefit and a reason why the feature requested by this issue should be implemented as a first step.

[^2]: apollo-tooling, codemodsquad/graphql-typegen.

scotttrinh commented 1 year ago

Is this still desirable given the queries generator gives us something pretty close to this experience without needing to jump through a bunch of tooling hoops to get good type information? If the desired outcome was just sanitized inputs rather than co-location, feels like the queries generator fits the bill.