ã½ã¦ã¾ã¦ã® Software Engineer ããã£ã¦ãã¾ãã@mookjp ã§ãã
8/10 ã®è¨äºãã¡ã«ã«ãªShopsã®æè¡ã¹ã¿ãã¯ã¨ããã®é¸å®çç±ãã§ã¯ãã¡ã«ã«ãª Shops ã®ã¢ã¼ããã¯ãã£ã«ã¤ãã¦ããã®å
¨ä½åãç´¹ä»ãã¾ããã
ãã®è¨äºã§ã¯ããã®ãã¡ã® BFFï¼Backend for Frontendï¼ ã¬ã¤ã¤ã¨ãã¦ç¨æãã GraphQL ãµã¼ãã«ã¤ãã¦ãNestJS ã使ã£ãå®è£
ä¾ã交ãã¦ç´¹ä»ãã¾ãã
- GraphQL ã¨ã¯
- GraphQL ãµã¼ãå¨è¾ºã®æ§æ
- NestJS ã¨ã¯
- NestJS 㧠Code First ãªã¹ãã¼ãå®ç¾©ããã
- DataLoader ã使ã£ã¦ Batch Request ã«å¯¾å¿ãã
- èªè¨¼æ å ±ã®åã渡ãã¨ã¢ã¯ã»ã¹å¶é
- ãããã«
GraphQL ã¨ã¯
GraphQL ã¯ãã¯ã©ã¤ã¢ã³ãã¢ããªã±ã¼ã·ã§ã³ãããæè»ã«å¿ è¦ãªãã¼ã¿ãåå¾ãããã¨ãã§ããããã«è¨è¨ãããã¯ã¨ãªè¨èªã§ããSQL ã¨ãSQL ãçºè¡ããã¯ã©ã¤ã¢ã³ãã¢ããªã±ã¼ã·ã§ã³ããã㦠SQL ãåãä»ãããã¼ã¿ãã¼ã¹ãµã¼ããã¤ã¡ã¼ã¸ãã¦ãããã¨ããããããããããã¾ããã
GraphQL ã使ããã¨ã«ãã£ã¦ãæ¢åã®ã¢ããªã±ã¼ã·ã§ã³ãµã¼ãã® API ä»æ§ãå¤æ´ããã«ã¯ã©ã¤ã¢ã³ãå´ã§åå¾ããããã¼ã¿ã®ã³ã³ããã¼ã«ãã§ãããã GraphQL ã¹ãã¼ãå®ç¾©ãå©ç¨ããªã¯ã¨ã¹ãã®æ£ãããäºåã«æ¤è¨¼ãããã¨ãã§ãããªã©ãæ§ã ãªã¡ãªãããå¾ããã¾ãã
GraphQL ãµã¼ãå¨è¾ºã®æ§æ
é£è¼ä¼ç»ã®ä»ã®è¨äºã§ããç´¹ä»ãã¦ãã¾ãããã¡ã«ã«ãª Shops ã® GraphQL ãµã¼ãå¨è¾ºã®æ§æã¯ä»¥ä¸ã®ããã«ãªã£ã¦ãã¾ãã
ã¢ãã¤ã«ã¯ã©ã¤ã¢ã³ãã Next.js ã使ã£ãããã³ãã¨ã³ããµã¼ãã¯ãGraphQL ãµã¼ãã¸ãªã¯ã¨ã¹ããéä¿¡ãããã¨ã«ãã£ã¦å¿
è¦ãªãã¼ã¿ã®åå¾ãæ´æ°ãè¡ã£ã¦ãã¾ãã
GraphQL ãµã¼ã㯠Microservices ã® gRPC API ãå¼ã³åºãã¾ãã
NestJS ã¨ã¯
8/10 ã®è¨äºã§ãç´¹ä»ãã¾ããããã¡ã«ã«ãª Shops ã§ã¯ NestJS ãã¬ã¼ã ã¯ã¼ã¯ã使ã£ã BFF ãæ¡ç¨ãã¦ãã¾ãã
NestJS 㯠Web ãµã¼ãã¼ã¨ãã¦ã®ãã¸ãã¯ã®å®è£
ãããç´ æ©ãããã¹ãããããä½ããã¨ãç®æãããã¬ã¼ã ã¯ã¼ã¯ã§ãï¼HTTP Server ã¨ãã¦ã®å®è£
㯠Express, fastify ãé¸ã¶ãã¨ãã§ãã¾ãï¼ã
ã¾ãããã¹ã¦ã®æ©è½ã¯ TypeScript ã§ã®è¨è¿°ããµãã¼ããã¦ãã¾ããã¡ã«ã«ãª Shops ã§ã¯ã TypeScript 㧠NestJS ãµã¼ãã®ã³ã¼ããè¨è¿°ãã¦ãã¾ãã
NestJS ã®ç¹å¾´ã¯ãModulesã®ä»çµã¿ã使ã£ã¦é©åã«åå²ãããæ©è½åä½ã§ã®éçºã容æã«ã§ãããã¨ã§ãã
NestJS ãæä¾ãããã³ã¬ã¼ã¿ã使ããã¨ã«ãããä¾åã®æ³¨å
¥ï¼Dependency Injectionãä»¥ä¸ DI ã¨è¡¨è¨ãã¾ãï¼ã¯ NestJS ã«ä»»ãããã¨ãã§ãã¾ãã
ä¾ãã°ããShopããAssetãã¨ããè¤æ°ã®æ©è½åä½ã«åå²ãã¦éçºãé²ãããShopServiceãã§ãAssetDataLoaderããå©ç¨ãããå ´åã¯ä»¥ä¸ã®ããã«ãã³ã¬ã¼ã¿ãä»ä¸ãããã¨ã«ãããDI ãå®ç¾ã§ãã¾ãã
// asset.module.ts
// â¦
@Module({
providers: [AssetService, AssetDataLoader],
exports: [AssetDataLoader],
})
export class AssetModule {}
// shop.module.ts
// â¦
@Module({
imports: [AssetModule],
providers: [ShopService, ShopDataLoader],
})
export class ShopModule {}
// shop.service.ts
// â¦
@Injectable()
export class ShopService {
constructor(private assetDataLoader: AssetDataLoader) {} // AssetModule ã§å
¬éããã AssetDataLoader ãå©ç¨
// ...
}
NestJS ã® DI ã«ã¤ãã¦ã¯ãããã¥ã¡ã³ãããµã³ãã«ã³ã¼ããããã¾ãã®ã§ãã¡ããèªãã§ã¿ãã¨ããã¤ã¡ã¼ã¸ãããããã¨æãã¾ãã
GraphQL Module
ã¡ã«ã«ãª Shops ã§ã¯ã NestJS ãæä¾ãã GraphQLModule ã使ã£ã¦ GraphQL ãµã¼ãã®å®è£ ãè¡ã£ã¦ãã¾ãã
ãã® GraphQLModule ã¯ã GraphQL ãµã¼ãã®å®è£
ã§ãã Apollo ãã©ãããããã®ã§ãã
GraphQLModule 㨠GraphQLModule ç¨ã®ãã³ã¬ã¼ã¿ãã³ã¼ãã«ä»ä¸ãããã¨ã§ãGraphQL ã¯ã¨ãªã®ãã£ã¼ã«ãã解決ããå®è£
ï¼ä»¥ä¸ãResolvers ã¨è¡¨è¨ãã¾ãï¼ãæ¸ããªãã GraphQL ã®ã¹ãã¼ãå®ç¾©ãèªåçæãããã¨ãã§ãã¾ãï¼Code First ãªã¹ãã¼ãå®ç¾©ã§ãï¼ã
Apollo ã®ããã¥ã¡ã³ãã§ã¯ GraphQL ã¹ãã¼ããå®ç¾©ããã®ã¡ã« Resolvers ãå®è£
ãã¦ããææ³ã説æããã¦ãã¾ãããGraphQLModule ã使ããã¨ã«ãã£ã¦ãã®ä½æ¥ãå¹çåãããã¨ãã§ãã¾ãã
次ã®ã»ã¯ã·ã§ã³ã§ã¯ãå®éã«ã©ã®ããã«ã³ã¼ããæ¸ãã¦ããããç´¹ä»ãã¾ãã
NestJS 㧠Code First ãªã¹ãã¼ãå®ç¾©ããã
ããã§ã¯å®éã«ã©ã®ãããªã³ã¼ããæ¸ã㦠GraphQL ã®ã¹ãã¼ãå®ç¾©ã¨ Resolvers ã®å®è£ ããã¦ããããç´¹ä»ãã¾ãã
ä»åã®ä¾ã§ã¯ã以ä¸ã®ãã㪠GraphQL ã¹ãã¼ãå®ç¾©ããResolvers ã®å®è£ ãããªããè¡ã£ã¦ããã¾ãã
type Asset {
// ...
}
type Shop {
name: String!
thumbnail: Asset
// ...
}
input ShopsInput {
// â¦
}
Input UpdateShopInput {
// â¦
}
type Query {
shops(shopsInput: ShopsInput!): [Shop!]!
}
type Mutation {
updateShop(updateShopInput: UpdateShopInput!): Boolean!
}
Object types ã®å®ç¾©
GraphQL ã® Object types ã¯ä»¥ä¸ã®ãããªã¯ã©ã¹ã§å®ç¾©ãããã¨ãã§ãã¾ãã
// shop.entity.ts
// â¦
import { Asset } from '../../asset/entities/asset';
// ...
@ObjectType()
export class Shop {
@Field(() => String)
name!: string;
@Field(() => Asset, { nullable: true })
thumbnail?: Asset; // import ã㦠å¥éå®ç¾©ããã Object types ãå©ç¨
// â¦
thumbnailId: string; // å¾è¿°ãã Resolvers å®ç¾©ã§èª¬æãã¾ã
// ...
}
@ObjectType()
ãã³ã¬ã¼ã¿ãä»ä¸ããã¯ã©ã¹ãã²ã¨ã¤ã® Object types ã¨ãã¦å®ç¾©ããã¾ããã¾ããObject types ã® fields 㯠@Field()
ãã³ã¬ã¼ã¿ãä»ä¸ãããã¨ã«ãã£ã¦å®ç¾©ã§ãã¾ãã
Query 㨠Mutation ã®å®ç¾©
Resolvers ã®å®è£
ãããã¨åæã«ãGraphQL ã¹ãã¼ãã® Query 㨠Muration ãå®ç¾©ãã¦ãããã¨ãã§ãã¾ãã
Resolvers ã®å®è£
ã¯ã@Resolver()
ãã³ã¬ã¼ã¿ãä»ä¸ããã¯ã©ã¹ãä½æãããã®ã¡ã½ããã« @Query()
, @Mutation()
ãã³ã¬ã¼ã¿ãä»ä¸ãããã¨ã§è¡ãã¾ãã
ã¾ããç¹å®ã®ãã£ã¼ã«ãã«ã¤ãã¦ã® Resolvers 㯠@ResolveField()
ãã³ã¬ã¼ã¿ãä»ä¸ãããã¨ã«ãã£ã¦å®è£
ã§ãã¾ãã
以ä¸ã®ã³ã¼ãã§ã¯ãShop type ã®ãã¼ã¿ã®åå¾ã ShopService ãä»ãã¦è¡ãã¤ã¤ãthumbnail ãã£ã¼ã«ãã®å
容ã®åå¾ã AssetDataLoader ãä»ãã¦è¡ãã¾ãã
ShopService ã§ã¯ protobuf ããçæãã gRPC client ã使ã£ã¦ Microservices ã® API ãå¼ã³åºãã¾ãï¼ã³ã¼ãä¾ã¯çç¥ãã¾ãï¼ã
AssetDataLoader 㯠ShopService ã¨åãã Microservices ã® API ãå¼ã³åºãã¦ãµã ãã¤ã«æ
å ±ãåå¾ãã¾ãããããããªã¯ã¨ã¹ããå®ç¾ããå®è£
ã¨ãªã£ã¦ãã¾ãï¼ãã®å¾ã®ãDataLoader ã使ã£ã¦ Batch Request ã«å¯¾å¿ãããã»ã¯ã·ã§ã³ã§èª¬æãã¾ãï¼ããObject types ã®å®ç¾©ãã§å®ç¾©ãã Shop ã¯ã©ã¹ã® thumbnailId ãã£ã¼ã«ãã¯ã GraphQL ã¹ãã¼ãã® thumbnail ãã£ã¼ã«ãã解決ããããã«å©ç¨ããã¾ãã
// shop.resolver.ts
// â¦
@Resolver(() => Shop)
export class ShopResolver {
constructor(
private readonly shopService: ShopService,
private readonly assetDataLoader: AssetDataLoader,
// ...
) {}
@Query(() => [Shop])
shops(
@Args('shopsInput', { type: () => ShopsInput }) shopsInput: ShopsInput
) {
return this.shopService.findShops(ctx, shopsInput);
}
@Mutation(() => Boolean)
updateShop(
@Args('updateShopInput') updateShopInput: UpdateShopInput
) {
return this.shopService.updateShop(ctx, updateShopInput);
}
@ResolveField(() => Asset, { nullable: true })
async thumbnail(@Parent() { thumbnailId }: Shop) {
if (!thumbnailId) {
return null;
}
try {
return this.assetDataLoader.load(thumbnailId);
} catch {
return null;
}
}
// â¦
}
GraphQL ã¹ãã¼ãã®çæ
Code First ã§å®ç¾©ãã GraphQL ã¹ãã¼ãã®ãã¡ã¤ã«çæã¯ã
- NestJS ãµã¼ããèµ·åãã
- æåã§ã¹ãã¼ããã¡ã¤ã«ãçæãã
ã®ããããã«ãã£ã¦è¡ããã¨ãã§ãã¾ãã
NestJS ãµã¼ããèµ·åãã¦çæããå ´å㯠GqlModuleOptions ã® autoSchemaFile ãè¨å®ãã¾ãã
ã¡ã«ã«ãª Shops ã§ã¯ã GraphQL ã¹ãã¼ãã®çæç¨ã®ã¹ã¯ãªãããã¡ã¤ã«ã nodejs_binary ã使ã£ã¦ Bazel ã® target ã¨ãã¦å®ç¾©ããå®è¡ãã¦ãã¾ãã
ã¹ãã¼ãã® Breaking Change ï¼ç ´å£çå¤æ´ï¼ãé²ã
ã¯ã©ã¤ã¢ã³ãã¨ãµã¼ãã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ã¨ãªã BFF ã¨ã㦠API ã®å¾æ¹äºææ§ãä¿ã¤ãã¨ã¯éè¦ã§ãã
8/10 ã®è¨äºãã¡ã«ã«ãªShopsã®æè¡ã¹ã¿ãã¯ã¨ããã®é¸å®çç±ãã§ã¯ã ããã¯ã¨ã³ãã® Microservices 群㫠gRPC ãµã¼ããæ¡ç¨ãã¦ããã¨ç´¹ä»ãã¦ãã¾ããã ãããã® gRPC ãµã¼ãã¯æå ã®éçºç°å¢ã«å ã㦠CI ã§ã Protobuf ãã¡ã¤ã«ã®å¤æ´ãæ¤è¨¼ãããã¨ã«ãã£ã¦ Breaking Change ãé²ãã§ãã¾ãï¼buf breaking ã使ã£ã¦ãã¾ãï¼ã
ããã¨åãã BFF 㯠GraphQL ã¹ãã¼ãã® Breaking Change ã GraphQL Inspector ã使ã£ã¦æ¤ç¥ãã¦ãã¾ãã
GraphQL Inspector 㯠CLI ã«å ã㦠GitHub Action ãæä¾ãã¦ãã¾ããéçºä¸ãæå
ã§ã¯ CLI ã«ããæ¤è¨¼ãè¡ãã CI ã§ã¯ GitHub Action ã«ããæ¤è¨¼ãè¡ãã¾ããGitHub Action ã§ã¯ Breaking Change ãçºçãã¦ããç®æã«ã¢ããã¼ã·ã§ã³ãã¤ãã¦ããã¾ãã
DataLoader ã使ã£ã¦ Batch Request ã«å¯¾å¿ãã
NestJS ã使ã£ã GraphQL ã® Resolvers ã®å®ç¾©æ¹æ³ã«ã¤ãã¦ã¯åè¿°ãã¾ãããé常ã«ã·ã³ãã«ãªã³ã¼ãã§è¡¨ç¾ãããã¨ãã§ãã¾ããã以ä¸ã®ãã㪠GraphQL ã¯ã¨ãªãæ¸ããå ´åãResolvers ã®å®è£ æ¹æ³ã«ãã£ã¦ã¯ã¨ã¦ãéå¹çã«ãªã£ã¦ãã¾ãã¾ãã
query {
shops(first: 100) { // Shop ã 100 件åå¾ããã
edges {
node {
name,
thumbnail { // åå¾ãã Shop ãã¨ã« thumbnails ãã£ã¼ã«ãã®å
容ãåå¾ãã
imageUrl
}
}
}
}
}
ãã£ã¼ã«ãã® Resolvers ã§ã¯ãGraphQL ã¯ã¨ãªå
¨ä½ã®ã³ã³ããã¹ãã¯ããããªãããã親ã®æ
å ±ã®ã¿ããã¨ã«ãã¦ãã¼ã¿ãè¿å´ããå¿
è¦ãããã¾ãã
ä¾ãã°ä»¥ä¸ã®ãã㪠Resolvers ã®å®è£
ã«ãã¦ãã¾ãã¨ãShop ã 100 件åå¾ããå ´å㯠Asset API ã 100 åå¼ã³åºããã¨ã«ãªãã¾ãã
// shop.resolver.ts
// â¦
@Resolver(() => Shop)
export class ShopResolver {
constructor(
private readonly assetService: assetService,
// ...
) {}
// â¦
@ResolveField(() => Asset, { nullable: true })
async thumbnail(@Parent() { thumbnailId }: Shop) {
return this.asserService.find(thumbnailId); // 1 件åå¾ãã Asset API å¼ã³åºã
}
// â¦
}
GraphQL ããã¥ã¡ã³ãã¼ã·ã§ã³ã®ãServer-side Batching & Cachingãã§ãç´¹ä»ããã¦ãã¾ããã Batch Request ï¼ä»¥ä¸ããããªã¯ã¨ã¹ãã¨è¡¨è¨ãã¾ããè¤æ°ã®ãªã½ã¼ã¹ãä¸åº¦ã«åå¾ããããã®ãªã¯ã¨ã¹ãã®ãã¨ãæå³ãã¾ãï¼ã¨ DataLoader ãå©ç¨ãããã¨ã«ãããã®åé¡ãåé¿ãããã¨ãã§ãã¾ãã
DataLoader ã¯ãã¤ãã³ãã«ã¼ãã®åä¸ãã¬ã¼ã å
ã§å®è¡ããããã¹ã¦ã® DataLoader.load(key)
ã® key
ããã²ã¨ã¤ã®ããããªã¯ã¨ã¹ãã®ãã©ã¡ã¼ã¿ã¨ãã¦ã¾ã¨ãã¦éä¿¡ãã¾ãã
以ä¸ã« gRPC ãµã¼ãã§ã® BatchGet API ã®å®ç¾©ä¾ã¨ DataLoader ã®å®è£ ä¾ãæãã¾ãã
BatchGet API ã®å®ç¾©
以ä¸ã¯ gRPC ãµã¼ãã® Protobuf è¨è¿°ä¾ã§ãã
ãã®ä¾ã§ã¯ãåå¾ããã Asset ãæå®ãã id ã®ãªã¹ãï¼ä»¥ä¸ã®ä¾ã®å ´å㯠ids ã該å½ãã¾ãï¼ãåãåã£ã¦ãAsset ã®ãªã¹ããè¿ã rpc ãå®ç¾©ãã¾ãã
// Protobuf ã«ãã gRPC ã® API å®ç¾©
// BatchGetAssets returns a list of requested assets.
rpc BatchGetAssets(BatchGetAssetsRequest) returns (BatchGetAssetsResponse) {}
message BatchGetAssetsRequest {
repeated string ids = 1;
}
message Asset {
// ...
}
message BatchGetAssetsResponse {
repeated Asset assets = 1;
}
// ...
NestJS 㧠DataLoader ãå©ç¨ãã
ç¨æãã ããããªã¯ã¨ã¹ã API ã å¼ã³åºã DataLoader ãå®è£
ãã¾ãã
DataLoader ã¯ã³ã³ã¹ãã©ã¯ã¿ã« BatchLoadFn é¢æ°ãåãåãã¾ããkey ã®ãªã¹ããåãåã£ã¦ãkey ã«å¯¾å¿ããçµæã®ãªã¹ããè¿ãã¨ããé¢æ°ã§ãã
ãã® BatchLoadFn é¢æ°å
㧠ããããªã¯ã¨ã¹ã API ãå¼ã³åºãã¾ãã
type BatchLoadFn<K, V> =
(keys: ReadonlyArray<K>) => PromiseLike<ArrayLike<V | Error>>;
Asset ã®ããã® DataLoader å®è£ ã¯ä»¥ä¸ã®ããã«ãªãã¾ãã
// asset.dataloader.ts
// ...
@Injectable({ scope: Scope.REQUEST })
export class AssetDataLoader extends DataLoader<string, Asset> {
constructor(private readonly assetService: AssetService) {
super((keys: string[]) => {
return this.assetService.findAll(keys); // BatchGetAssets ã®å¼ã³åºã
});
}
}
thumbnail ãã£ã¼ã«ãã®è§£æ±ºã«ã¯ããã® AssetDataLoader ã® load()
ã使ãã¾ããload()
㯠DataLoader ããç¶æ¿ããã¡ã½ããã§ãã
// shop.resolver.ts
// â¦
@Resolver(() => Shop)
export class ShopResolver {
constructor(
private readonly assetDataLoader: AssetDataLoader,
// ...
) {}
// â¦
@ResolveField(() => Asset, { nullable: true })
async thumbnail(@Parent() { thumbnailId }: Shop) {
if (!thumbnailId) {
return null;
}
try {
return this.assetDataLoader.load(thumbnailId);
} catch {
return null;
}
}
// â¦
}
DataLoader.load(key)
ã¯å
é¨ã§ BatchLoadFn ã使ã£ã¦åå¾ããçµæã key ãã¨ã® Promise ã«ãã¦è¿ãã¾ãã
DataLoader ã¯ããã©ã«ãè¨å®ã 㨠BatchLoadFn ã«æ¸¡ã key ã®æ°ãå¶éãã¾ãããããã¯ã¨ã³ã API ã®ä»æ§ã¨ã㦠key ã®æ大æ°ãå¶éãã¦ãã㨠DataLoader.load(key)
ã®å¼ã°ããåæ°ã«ãã£ã¦ã¯ã¨ãªã®å®è¡ã失æãã¦ãã¾ãå ´åãããã¾ãã
ä¾ãã°ãä¸è¨ã® gRPC API ã®å®ç¾©ã® BatchGetAssets
ããBatchGetAssetsRequest.ids
ã 1000 件以ä¸ã ã¨ã¨ã©ã¼ã«ãªã£ã¦ãã¾ããããªä»æ§ã ã¨ãDataLoader.load(key)
ã 1001 åå¼ã°ãã㨠DataLoader 㯠BatchGetItemsRequest.ids
ã« 1001 件㮠key ãæå®ãã¦ãªã¯ã¨ã¹ããéä¿¡ãã¦ãã¾ãã¾ãã
ãã®ãããªãã¨ãé²ãããã BatchLoadFn ã§å¼ã³åºãããã¯ã¨ã³ãã® API ã®ä»æ§ã«åããã¦ã maxBatchSize
ãªãã·ã§ã³ ãæå®ãã¦ããã¾ããDataLoader 㯠maxBatchSize
ã«éããæç¹ã§ããããªã¯ã¨ã¹ããåãã¦ããã¾ãã
ã¾ããã¡ã«ã«ãª Shops ã§ã¯ãMicroservices ã«ãªã¯ã¨ã¹ããéä¿¡ãã DataLoader ã¨ãRedis ããã£ãã·ã¥ã¨ãã¦å©ç¨ãã DataLoader ã¨ãåå¾å¯¾è±¡ã®ãªã½ã¼ã¹ã«ãã使ãåãã¦ãã¾ããæ´æ°é »åº¦ãé«ããªããã¼ã¿ã«ã¤ãã¦ã¯å¾è ã使ããã¨ã«ãã£ã¦é«éåãå³ã£ã¦ãã¾ãã
èªè¨¼æ å ±ã®åã渡ãã¨ã¢ã¯ã»ã¹å¶é
ããã³ãã¨ã³ãã¨ããã¯ã¨ã³ãã® Microservices ã®éã«åå¨ãã BFF ã¬ã¤ã¤ã§ã¯ãèªè¨¼æ å ±ã®åã渡ãã¨ããã®èªè¨¼æ å ±ã«å¿ããã¢ã¯ã»ã¹å¶éãå¿ è¦ã§ãã
ã¡ã«ã«ãª Shops ã§ã¯ GraphQL ã®ã¯ã¨ãªå
容ã«å¿ãã¦ããªã¯ã¨ã¹ãã«å¿
è¦ãªèªè¨¼æ
å ±ãå«ã¾ãã¦ããããã¾ãå¿
è¦ãªæ¨©éãããããæ¤è¨¼ãã¦ãã¾ãã
å
·ä½çã«ã¯ãNestJS ã® ï¼ Resolver() ã³ã³ãã¼ãã³ãã§ãã¢ã¯ã»ã¹å¶éã®ããã«å®è£
ãããã³ã¬ã¼ã¿ã使ããã¨ã«ãã£ã¦ãããå®ç¾ãã¦ãã¾ãã
// shop.resolver.ts
// â¦
@Mutation(() => Boolean)
@RequireAuth(âroleâ) // @RequireAuth ã§èªè¨¼æ
å ±ã®æ¤è¨¼
updateShop(
@AuthContext() ctx: AuthContext, // @AuthContext 㧠ctx ã«èªè¨¼æ
å ±ãå«ãã
@Args('updateShopInput') updateShopInput: UpdateShopInput
) {
return this.shopService.updateShop(ctx, updateShopInput); // gRPC API å¼ã³åºãæã«èªè¨¼æ
å ±ã渡ã
}
ã«ã¹ã¿ã ãã³ã¬ã¼ã¿ã® @RequireAuth()
㨠@AuthContext()
ã¯ä»¥ä¸ã®ãããªå®è£
ã«ãªãã¾ãã
import {
applyDecorators,
CanActivate,
ExecutionContext,
Inject,
Injectable,
SetMetadata,
UseGuards,
} from '@nestjs/common';
export function RequireAuth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard)
);
}
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
@Inject(AuthService) private authService: AuthService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const req: Request = ctx.getContext().req;
// req ããèªè¨¼æ
å ±ãåå¾ãã
// èªè¨¼æ
å ±ã®æ¤è¨¼ã role ãåå¾ãã
// reflector ã使ã£ã¦å¶éãã role ãåå¾ãã
const requiredRoles = this.reflector.get<Role[]>('roles', ctx.getHandler())
// roles ã®æ¤è¨¼
// â¦
}
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express'
import { Metadata } from 'grpc';
// ...
export type AuthContext = {
metadata?: Metadata; // gRPC API å¼ã³åºãæã«èªè¨¼æ
å ±ã渡ããã Metadata ãã»ãããã¦ãã
};
export const AuthContext = createParamDecorator(
async (data: unknown, ctx: ExecutionContext): Promise<AuthContext> => {
const gqlctx = GqlExecutionContext.create(ctx);
const req: Request = gqlctx.getContext().req;
const metadata = new Metadata();
// req ã®å
容ããã¨ã« Metadata.set() ã§èªè¨¼æ
å ±ãã»ãããã
return {
metadata: metadata,
};
}
);
ãããã«
ãã®è¨äºã§ã¯ãã¡ã«ã«ãª Shops ã§ã® NestJS ã使ã£ã GraphQL Server ã®å®è£
ã«ã¤ãã¦ãç´¹ä»ãã¾ããã
BFF ã¬ã¤ã¤ã®å®è£
ã«ããã¦ã¯ãã©ã®ãããªææ³ã使ãã®ããGraphQL ãµã¼ããæ¡ç¨ããã®ããã¾ã GraphQL ãµã¼ããæ¡ç¨ãããªãã©ã®å®è£
ã使ããããªã©æ§ã
ãªé¸æè¢ããããã¨æãã¾ããããã®è¨äºãåèã«ãªãã¾ããã幸ãã§ãã
ã¾ãã8æ18æ¥ï¼æ°´ï¼ ãããã½ã¦ã¾ã¦ TECH TALKãã¨ããã¤ãã³ããéå¬ããã¦ãã¾ããã9æ8æ¥ï¼æ°´ï¼ã® #04:Backends For Frontends ã®åã§ã¯ä»åã®è©±é¡ãæ±ãã¾ãã®ã§ãèå³ãããã°ãåå ããã ããã¨ããããã§ãã
ã½ã¦ã¾ã¦ TECH TALK ã§ã¯ Backends For Frontends 以å¤ã«ãæ§ã
ãªãã¼ããåãæ±ã£ã¦ãã¾ãã®ã§ãä»ã«ãæ°ã«ãªãåãããã¾ããããã²ãåå ãã ããã
ã¡ã«ã«ãª Shops ãéçºã»éç¨ãã¦ããæ ªå¼ä¼ç¤¾ã½ã¦ã¾ã¦ã§ã¯çµ¶è³æ¡ç¨ä¸ã§ãã
ã½ããã¦ã§ã¢ã¨ã³ã¸ãã¢ã ãã§ã¯ãªããæ§ã
ãªè·ç¨®ã§åéãã¦ãã¾ãããã¸ã·ã§ã³ã¯ãã¡ããåç
§ãã¦ãã ãããèå³ããã£ã¦è©±ãèãã¦ã¿ãããã¨ããå ´åã¯ã«ã¸ã¥ã¢ã«1on1ãè¨å®ãã¦ããã¾ãã®ã§ãã¡ãããæ¤è¨ãã ããã
ææ¥ 8æ20æ¥ã¯ Software Engineer @wakanapo ããã®ãã¡ã«ã«ãªShopsã®MLç«ã¡ä¸ã奮éè¨ããå ¬éäºå®ã§ããã楽ãã¿ã«ï¼