gqlを自分で記述したくない
思うことをここに箇条書きしてみる
- gqlを記述したくない
- 本質的には、何らかのresourceを定義して、そのそれぞれの型のサブセットを返すようなqueryがgraphなのでは?
- それを記述する方法は別にgraphqlの記法に縛られる必要はない気もする
- ただし全力でvalidationも補完も効いてほしい
- DX(開発者体験)の良さは、事前チェックや随時チェックにあるような気がする。
- ふわっとした記述でparse error (syntax error)とか嫌。
- つまり現代においてはlanguage serverの対応が必須
- 既存の言語で記述して、language serverは間借りする (e.g. AWS CDK)
- あまりきれいにはならない。
- 自分で言語を作って、language server周りのエコシステムも頑張って整える
- 勢いがあるうち良いが、メンテナンスが滞ったりしたときに困る
- 既存の言語で記述して、language serverは間借りする (e.g. AWS CDK)
- DX(開発者体験)の良さは、事前チェックや随時チェックにあるような気がする。
resource
objectの定義
これは普通に焼き直しになりそう。例えば以下のような記述がほしいとする。
type Todo {
id: ID!
text: String!
done: Boolean! @hasRole(role: OWNER) # only the owner can see if a todo is done
}
普通に記述するだけそう。
:memo: ディレクティブの部分をどうするかは後で考える必要がある。
openapigenを拝借して例えば以下のような感じ。
var b = openapigen.NewBuilder(openapigen.DefaultConfig())
var (
Todo = b.Object(
b.Field("id", b.String()), // 正確にはID型
b.Field("text", b.String()),
b.Field("done", b.Bool()).Doc("only the owner can see if a todo is done"),
)
問題はこれだとTodo.Idみたいな形で補完が効かないところ(一旦はすべてexportedフィールドで考えることにする)
resource packageに以下のような値を用意してみる。
package resource
type Field struct {
Name string
Type string
Required bool
}
type _TodoDefinition struct { // exportしてしまうと補完候補に出てしまう
ID Field
Text Field
Done Field
}
var Todo = _TodoDefinition{
ID: Field{Name: "id", Type: "ID", Required: true},
Text: Field{Name: "name", Type: "String", Required: true},
Done: Field{Name: "done", Type: "Boolean", Required: true},
}
基本的にフィールドは全部unexportedのほうが嬉しいのかもしれない。そうしないと補完の候補に載ってしまいうるさくなる。
Todoはいい感じ。
フィールド名の先の情報は隠れてほしい (e.g. Required)
サブセットがほしい場合ってどういうときなんだろう? inputはサブセットが欲しくなる?
"Passed to createTodo to create a new todo"
input TodoInput {
"The body text"
text: String!
"Is it done already?"
done: Boolean
}
ネストしたpaginationのようなものを使いたいときには返す値が定義時のオブジェクトをwrappingした型になりそう。
これにargumentsを渡す方法は?
普通にフィールドがargumentsを取れる
queryの定義
argumentsとかが現れる。どうやって扱うと良いんだろう?
type MyQuery {
todo(id: ID!): Todo
lastTodo: Todo
todos: [Todo!]!
}
goにはキーワード引数などはない。
resource
type _Field struct {
name string
typ Type
}
type typImpl struct {
name string
required bool
}
func (t typImpl) typ()
// primitives
var (
ID = typImpl{"ID", true}
String = typImpl{"String", true}
Boolean = typImpl{"Boolean", true}
)
type _TodoDefinition struct { // exportしてしまうと補完候補に出てしまう
typImpl
ID _Field
Text _Field
Done _Field
}
var Todo = _TodoDefinition{
typImpl: typImpl{name: "Todo"},
ID: _Field{name: "id", typ: ID},
Text: _Field{name: "name", typ: String},
Done: _Field{name: "done", typ: Boolean},
}
structの手書きは人間がやるものじゃない。primitiveな型も定義しておいてあげる必要がありそう。
type Query struct {
Name string
Attributes []Attribute
Return resource.Type
}
type Attribute struct {
Name string
Type resource.Type
}
func use() {
myQuery := []Query{
{Name: "todo", Attributes: []Attribute{{Name: "id", Type: resource.ID}}, Return: resource.Todo},
{Name: "lastTodo", Return: resource.Todo},
{Name: "todos", Return: resource.Todo}, // TODO: array
}
_ = myQuery
}
こんなコードで記述できれば良いんだろうか?
myquery2 = Query(
Node("todo", resource.Todo).Arg("id", resource.ID),
Node("lastTodo", resource.Todo)),
Node("todos", ArrayOf(resource.Todo)),
)
もう少し複雑なネストした型だとどうだろう?
query自体はネストするけど、queryの定義自体はネストしないのだっけ? ↑で言えばTodoがさらにメソッドを持っていたりするときにそれを絞れるみたいな感じなのだっけ?
queryを呼び出すとき
variables経由なのはそれはそうと言う感じ。
- Fragments
- Arguments (ith default value)
- Aliases
- advanced type (enums, interfaces, union types)
こんな書き方も。
{
allPersons {
name # works for `Adult` and `Child`
... on Child {
school
}
... on Adult {
work
}
}
}
大変になるのはqueryを書くときなのだっけ?
nested object
{
topic(name:"graphql") {
stargazerCount
relatedTopics {
name
stargazerCount
}
}
}