Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Template tag for EdgeQL #350

Open
colinhacks opened this issue May 12, 2022 · 10 comments
Open

Template tag for EdgeQL #350

colinhacks opened this issue May 12, 2022 · 10 comments

Comments

@colinhacks
Copy link
Contributor

colinhacks commented May 12, 2022

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:

  • Array.isArray(data) === true -> check that contents are homogenous primitives -> array<element-type>
  • object -> json
  • string -> str
  • number -> float64
  • bigint -> bigint
  • boolean -> bool

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
Copy link
Member

1st1 commented May 12, 2022

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
Copy link
Contributor Author

colinhacks commented May 13, 2022

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
Copy link
Member

elprans commented May 14, 2022

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
Copy link
Member

jaclarke commented Jun 6, 2022

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
Copy link
Contributor Author

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

Indeed good call!

@infogulch
Copy link

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
Copy link
Contributor Author

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
Copy link

haikyuu commented Jul 14, 2022

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
Copy link

infogulch commented Jul 15, 2022

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:

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

Footnotes

  1. apollo-tooling, codemodsquad/graphql-typegen.

@scotttrinh
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants