🪀

Fuse: TypeScript API Frameworkを使ってみる

2024/07/07に公開

日本語のしっかりした記事はまだなかったので日本初です

Fuse: TypeScript API Framework

https://fusedata.dev/

https://github.com/StellateHQ/fuse

導入

npx create-fuse-app
npx fuse dev

http://localhost:4000/graphql にアクセスするとGraphiQLが起動するようになっている

デモ用のschema

type Mutation {
  _version: String!
}

interface Node {
  id: ID!
}

type Query {
  _version: String!
  node(id: ID!): Node
  nodes(ids: [ID!]!): [Node]!
  user(id: ID!): User
}

type User implements Node {
  avatarUrl: String
  firstName: String
  id: ID!
  name: String
}

types/User.ts

import { node } from 'fuse'
 
type UserSource = {
  id: string
  name: string
  avatar_url: string
}
 
// "Nodes" are the core abstraction of Fuse. Each node represents
// a resource/entity with multiple fields and has to define two things:
// 1. load(): How to fetch from the underlying data source
// 2. fields: What fields should be exposed and added for clients
export const UserNode = node<UserSource>({
  name: 'User',
  load: async (ids) => getUsers(ids),
  fields: (t) => ({
    name: t.exposeString('name'),
    // rename to camel-case
    avatarUrl: t.exposeString('avatar_url'),
    // Add an additional firstName field
    firstName: t.string({
      resolve: (user) => user.name.split(' ')[0],
    }),
  }),
})
 
// Fake function to fetch users. In real applications, this would
// talk to an underlying REST API/gRPC service/third-party API/…
async function getUsers(ids: string[]): Promise<UserSource[]> {
  return ids.map((id) => ({
    id,
    name: `Peter #${id}`,
    avatar_url: `https://i.pravatar.cc/300?u=${id}`,
  }))
}

Queries and Mutations

Query

addQueryFields を使用する

import { addQueryFields } from 'fuse'
 
addQueryFields(t => ({
  me: t.field({
    type: UserNode,
    resolve: (parent, args, context) => context.userId
  })
}))

Mutation

addMutationFields を使用する

import { addMutationFields } from 'fuse'
 
addMutationFields(t => ({
  sayHello: t.field({
    type: 'String',
    args: {
      name: t.arg.string({ required: true })
    },
    resolve: (parent, args, context) => `Hello ${args.name}!`
  })
}))

Arguments

デフォルトでoptionalだが、必須にしたい場合は { required: true } を使用する

args: {
  name: t.arg.string({ required: true })
}

Lists

t.list と t.connection
t.connection はカーソルページネーション

list

import { addQueryFields } from 'fuse'
 
addQueryFields((t) => ({
  users: t.list({
    type: UserNode,
    nullable: false,
    args: {
      offset: t.arg.int(),
      limit: t.arg.int(),
    },
    resolve: async (_, args, context) => {
      const result = await getUsers({
        skip: args.offset,
        take: args.limit,
      })
 
      return {
        nodes: result.users,
        totalCount: result.totalCount,
      }
    },
  }),
}))
type UserList {
  nodes: [User]!
  totalCount: Int
}
 
type Query {
  users(limit: Int, offset: Int): UserList!
}

connection

import { addQueryFields } from 'fuse'
 
addQueryFields((t) => ({
  users: t.connection({
    type: UserNode,
    nullable: false,
    resolve: async (_, args, context) => {
      const result = await getUsers({
        skip: args.offset,
        take: args.limit,
      })
 
      return {
        edges: result.users.map(user => ({
          node: user,
          cursor: user.id,
        })),
        pageInfo: getPageInfo(result),
      }
    },
  }),
}))
type UserEdge {
  cursor: String!
  node: User!
}
 
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String!
  endCursor: String!
}
 
type UserConnection {
  edges: [UserEdge]!
  pageInfo: PageInfo!
}
 
type Query {
  users(after: String, before: String, first: Int, last: Int): QueryLaunchesConnection!
}

カーソルページネーションの引数は自動的に含まれる

connectionを試してみる

次ページ

pageInfoも正常に機能している

schemaとintrospectionについて

自動的に定義される

schema

schema.graphql

introspection

fuse/introspection.ts

GraphQLSPを使っている
https://github.com/0no-co/GraphQLSP

DB (ORM)

Prismaの場合

テーブルからの取得

import { node, NotFoundError, addQueryFields } from 'fuse'
import { User } from '@prisma/client'
import { prisma } from './db'
 
export const UserNode = node<User>({
  name: 'User',
  async load(ids) {
    const result = await prisma.user.findMany({
      where: {
        id: {
          in: ids
        }
      }
    })
    return ids.map((id) => result.find((x) => x.id === id) || new NotFoundError('Could not find user.'));
  },
  fields: (t) => ({
    name: t.exposeString('name'),
  }),
})

ページネーションを含むList

t.listを使用してページネーションのListを作成するには、
nodesとtotalCountの両方を返す必要がある

addQueryFields((t) => ({
  users: t.list({
    type: UserNode,
    nullable: false,
    args: {
      offset: t.arg.int(),
      limit: t.arg.int(),
    },
    resolve: async (_, args) => {
      const offset = args.offset || 0
      const limit = args.limit || 10
 
      const [totalCount, paginatedUsers] = await Promise.all([
        // Get the total count of users as an int
        prisma.user.count()
        // Get the list of users based on the limit and offset
        prisma.user.findMany({
          skip: offset,
          take: limit
        })
      ])
 
      return {
        nodes: paginatedUsers,
        totalCount: totalCount[0].count,
      }
    },
  }),
}))

Cloud SQL for PostgreSQL + Prisma

Prismaの初期化

npx prisma init

.envファイル

DB_USER=
DB_PORT=
DB_PASSWORD=
DB_NAME=
DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}"

Cloud SQLへのローカルからの接続はProxy経由でないとできないので
DATABASE_URLは localhost:5432 をみる

prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

prismaCopydatasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

cloud-sql-proxy

brew install cloud-sql-proxy
gcloud auth login

gcloud config set project [PROJECT_ID]

cloud-sql-proxy [接続名]

2024/07/08 17:38:48 Authorizing with Application Default Credentials
2024/07/08 17:38:50 [接続名] Listening on 127.0.0.1:5432
2024/07/08 17:38:50 The proxy has started successfully and is ready for new connections!

schema取得

npx prisma db pull

validate

❯ npx prisma validate

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
The schema at prisma/schema.prisma is valid 🚀

Prisma Client生成

npx prisma generate

Discussion