Skip to main content
Version: 5.3

Side Quest: How Redwood Works with Data

RedwoodはGraphQLが好きです。私たちは、これが未来のAPIだと考えています。私たちのGraphQL実装は、クライアント側はApollo、サーバ側はGraphQL Yoga & Envelopで構築されています。ファイルのレイアウト(配置)で、ディレクトリ api/src/functions があり、その中に graphql.ts が1つあったことを思い出してください。もし、あなたのアプリを serverless スタックにデプロイするとしたら(これは後々 Deployment セクションで実施します)、その graphql.ts ファイルはサーバレス関数にコンパイルされて GraphQL API のエンドポイントになることでしょう。以下は、典型的な GraphQL クエリがどのようにアプリを通過していくかを示しています:

Redwood Data Flow

フロントエンドでは Apollo Client を使って、GraphQL YogaEnvelop に送る GraphQL ペイロードを作成し、その graphql.ts ファイルがエントリポイントとして機能します。

api/src/graphql にある *.sdl.ts ファイルは、 GraphQL の Object型Query型Mutation型 、つまりあなたの API のインターフェースを定義しています。

通常、すべてのリゾルバを含む resolver map を作成し、GraphQL サーバに SDL へのマッピング方法を説明します。しかし、ビジネスロジックをリゾルバマップに直接記述すると、 ファイルが非常に大きくなり、再利用性も悪くなります。そこで、すべてのロジックを関数のライブラリに展開し、 それをインポートしてリゾルバマップから呼び出し、すべての引数を渡すようにするのがよいでしょう。しかし、これは多くの労力と定型的な処理を必要とする上に、再利用性の観点であまり良い結果になりません。

Redwood にはもっといい方法があります! api/src/services ディレクトリを覚えていますか?Redwood は、対応する services ファイルからリゾルバを自動的にインポートして SDL にマップします。同時に、これらのリゾルバを他のリゾルバやサービスから、通常の関数として簡単に呼び出せるように書くことができます。それでは、例を挙げてみましょう。

次の SDL JavaScript のスニペットを考えてみましょう:

api/src/graphql/posts.sdl.js
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}

type Query {
posts: [Post!]!
post(id: Int!): Post!
}

input CreatePostInput {
title: String!
body: String!
}

input UpdatePostInput {
title: String
body: String
}

type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`

この例では、Redwood は api/src/services/posts/posts.ts から次の5つのリゾルバを探します:

  • posts()
  • post({ id })
  • createPost({ input })
  • updatePost({ id, input })
  • deletePost({ id })

これらを実装するには、サービスファイルで単にエクスポートします。これらは通常、データベースからデータを取得しますが、 posts.sdl.ts で定義した内容に基づいて GraphQL Yoga が期待する適切な型を返す限り、何でもできます。

api/src/services/posts/posts.js
import { db } from 'src/lib/db'

export const posts = () => {
return db.post.findMany()
}

export const post = ({ id }) => {
return db.post.findUnique({
where: { id },
})
}

export const createPost = ({ input }) => {
return db.post.create({
data: input,
})
}

export const updatePost = ({ id, input }) => {
return db.post.update({
data: input,
where: { id },
})
}

export const deletePost = ({ id }) => {
return db.post.delete({
where: { id },
})
}
info

Yoga/Envelop は、これらの関数が dbPrismaClient のインスタンス) からPromiseを返すことを想定しています。Yoga/Envelop はこれらの関数が解決するのを待ってからクエリ結果を返すので、 async / await を気にしたり、コールバックをいじったりする必要はありません。

なぜこれらの実装ファイルを "サービス" と呼ぶのか不思議に思うかもしれません。このブログの例では、それがわかるほど複雑になっていませんが、サービスは、単一のデータベーステーブルの にある抽象化を意図しています。例えば、もう少し複雑なアプリでは "billing" サービスが transactions テーブルと subscriptions テーブルを2つとも使うかもしれません。このサービスの機能の一部は、GraphQLを介して公開することができますが、それはあなたが望む範囲に限られます。

サービスの各機能をGraphQLで利用できるようにする必要はありません。QueryMutation の型定義から除外すれば、GraphQLの範囲内では存在しないことになります。しかし、自分では使うことができます。サービスは単なるJavaScriptの関数なので、どこでも好きなように使うことができます:

  • 他のサービスから
  • カスタムLambda関数の中で
  • 完全に独立したカスタムAPIから

アプリを明確に定義されたサービスに分割し、それらのサービスに対するAPI(内部使用とGraphQLのいずれも)を提供することで、関心事の分離が自然に行われるようになり、コードベースの保守性が向上します。

データフローに話を戻しましょう:Yoga/Envelop はリゾルバを呼び出し、この場合、データベースからデータを取得しました。Yoga/Envelopはオブジェクトを調査し、GraphQLクエリで要求されたキー/値のみを返します。そして、レスポンスを GraphQL ペイロードにパッケージングして、ブラウザに返します:

Redwoodの セル を使用している場合、このデータは他のReactコンポーネントと同様に Success コンポーネントでループしたり表示したりできます。