🪀
Fuse: TypeScript API Frameworkを使ってみる
日本語のしっかりした記事はまだなかったので日本初です
Fuse: TypeScript API Framework
導入
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を使っている
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