From 798e66db663b1b35df6c49b658288a2dbd1b66c0 Mon Sep 17 00:00:00 2001 From: dayuy <973860441@qq.com> Date: Fri, 24 Feb 2023 17:14:14 +0800 Subject: [PATCH] fix: add fields channel and network of IBPPeer API(#13) --- src/channel/channel.service.ts | 31 +++-- src/common/utils/tools.ts | 7 ++ src/ibppeer/ibppeer.gql | 15 +++ src/ibppeer/ibppeer.module.ts | 3 +- src/ibppeer/ibppeer.resolver.ts | 109 +++++++++++++++++- src/ibppeer/ibppeer.service.ts | 1 + src/ibppeer/models/ibppeer.model.ts | 10 +- src/network/network.resolver.ts | 4 +- src/organization/models/organization.model.ts | 3 + src/organization/organization.gql | 14 +-- src/organization/organization.resolver.ts | 22 ++++ src/schema.gql | 15 +++ 12 files changed, 201 insertions(+), 33 deletions(-) diff --git a/src/channel/channel.service.ts b/src/channel/channel.service.ts index e889d75..544df32 100644 --- a/src/channel/channel.service.ts +++ b/src/channel/channel.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@nestjs/common'; -import { filter, find, isEqual, uniqWith } from 'lodash'; +import { Injectable, Logger } from '@nestjs/common'; +import { filter, find, isEqual, uniq, uniqWith } from 'lodash'; import { KubernetesService } from 'src/kubernetes/kubernetes.service'; import { CRD } from 'src/kubernetes/lib'; import { JwtAuth } from 'src/types'; @@ -12,6 +12,8 @@ import { Channel } from './models/channel.model'; export class ChannelService { constructor(private readonly k8sService: KubernetesService) {} + private logger = new Logger('ChannelService'); + format(channel: CRD.Channel): Channel { return { name: channel.metadata.name, @@ -37,19 +39,28 @@ export class ChannelService { return this.format(body); } + async getChannelsByNames(auth: JwtAuth, names: string[]): Promise { + const res = await Promise.allSettled( + uniq(names).map((n) => n && this.getChannel(auth, n)), + ); + const chans = []; + res?.forEach((r) => { + if (r.status === 'fulfilled') { + chans.push(r.value); + } else { + this.logger.error('Failure', r.reason?.body); + } + }); + return chans; + } + async createChannel( auth: JwtAuth, network: string, channel: NewChannel, ): Promise { - const { - name, - description, - initiator, - organizations, - peers, // TODO:必须是用户管理的组织(在members中)下的节点(提供接口) - policy, - } = channel; + const { name, description, initiator, organizations, peers, policy } = + channel; const members = (organizations || []) .concat(initiator) .map((d) => ({ name: d })); diff --git a/src/common/utils/tools.ts b/src/common/utils/tools.ts index 723f264..c85129b 100644 --- a/src/common/utils/tools.ts +++ b/src/common/utils/tools.ts @@ -4,6 +4,7 @@ import { customAlphabet } from 'nanoid'; import { numbers, lowercase } from 'nanoid-dictionary'; import { TokenException } from './errors'; import type { JwtAuth, Request } from '../../types'; +import { compact, uniq } from 'lodash'; /** * 从 token 中解析用户认证信息 @@ -82,3 +83,9 @@ export const nanoid = customAlphabet(numbers + lowercase, 5); * @returns */ export const genNanoid = (prefix: string) => `${prefix}-${nanoid()}`; + +/** + * 多层级数组平铺去重 + * @param {string[][]} arr + */ +export const flattenArr = (arr: string[][]) => uniq(compact(arr.flat())); diff --git a/src/ibppeer/ibppeer.gql b/src/ibppeer/ibppeer.gql index 141992f..6ec2a77 100644 --- a/src/ibppeer/ibppeer.gql +++ b/src/ibppeer/ibppeer.gql @@ -1,3 +1,18 @@ +# 获取节点列表 +query getIbppeers($organization: String!) { + ibppeers(organization: $organization) { + name + creationTimestamp + status + limits { + cpu + memory + } + channels + networks + } +} + # 创建节点 mutation createIbppeer($organization: String!) { ibppeerCreate(organization: $organization) { diff --git a/src/ibppeer/ibppeer.module.ts b/src/ibppeer/ibppeer.module.ts index c13ea82..f5a8e23 100644 --- a/src/ibppeer/ibppeer.module.ts +++ b/src/ibppeer/ibppeer.module.ts @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'; import { IbppeerService } from './ibppeer.service'; import { IbppeerResolver } from './ibppeer.resolver'; import { ConfigmapModule } from 'src/configmap/configmap.module'; +import { ChannelModule } from 'src/channel/channel.module'; @Module({ providers: [IbppeerService, IbppeerResolver], exports: [IbppeerService], - imports: [ConfigmapModule], + imports: [ConfigmapModule, ChannelModule], }) export class IbppeerModule {} diff --git a/src/ibppeer/ibppeer.resolver.ts b/src/ibppeer/ibppeer.resolver.ts index 497cfd6..ee17850 100644 --- a/src/ibppeer/ibppeer.resolver.ts +++ b/src/ibppeer/ibppeer.resolver.ts @@ -1,12 +1,41 @@ -import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { + Args, + Mutation, + Parent, + Query, + ResolveField, + Resolver, +} from '@nestjs/graphql'; +import DataLoader from 'dataloader'; +import { find } from 'lodash'; +import { ChannelService } from 'src/channel/channel.service'; +import { Loader } from 'src/common/dataloader'; import { Auth } from 'src/common/decorators/auth.decorator'; +import { flattenArr } from 'src/common/utils'; +import { FederationLoader } from 'src/federation/federation.loader'; +import { Federation } from 'src/federation/models/federation.model'; +import { Network } from 'src/network/models/network.model'; +import { NetworkLoader } from 'src/network/network.loader'; +import { Organization } from 'src/organization/models/organization.model'; +import { OrganizationLoader } from 'src/organization/organization.loader'; import { JwtAuth } from 'src/types'; import { IbppeerService } from './ibppeer.service'; import { Ibppeer } from './models/ibppeer.model'; -@Resolver() +@Resolver(() => Ibppeer) export class IbppeerResolver { - constructor(private readonly ibppeerService: IbppeerService) {} + constructor( + private readonly ibppeerService: IbppeerService, + private readonly channelService: ChannelService, + ) {} + + @Query(() => [Ibppeer], { description: '获取组织下的节点列表' }) + async ibppeers( + @Auth() auth: JwtAuth, + @Args('organization', { description: '所在组织' }) org: string, + ): Promise { + return this.ibppeerService.getIbppeers(auth, org); + } @Mutation(() => Ibppeer, { description: '创建IBPPeer节点' }) async ibppeerCreate( @@ -15,4 +44,78 @@ export class IbppeerResolver { ): Promise { return this.ibppeerService.createIbppeer(auth, org); } + + @ResolveField(() => [String], { + nullable: true, + description: '节点加入的通道', + }) + async channels( + @Auth() auth: JwtAuth, + @Parent() ibppeer: Ibppeer, + @Loader(OrganizationLoader) + organizationLoader: DataLoader, + @Loader(FederationLoader) + fedLoader: DataLoader, + @Loader(NetworkLoader) + networkLoader: DataLoader, + ): Promise { + // org -> fed -> net -> chan -> peers => chan + const { namespace, name } = ibppeer; + const { federations } = await organizationLoader.load(namespace); + if (!federations || federations.length === 0) return; + const feds = await fedLoader.loadMany(federations); + const networkNames = (feds as Federation[]).map((fed) => fed?.networkNames); + if (!networkNames || networkNames.length === 0) return; + const nets = await networkLoader.loadMany(flattenArr(networkNames)); + const channelNames = (nets as Network[]).map((net) => net.channelNames); + // TODO: channelLoader + const chans = await this.channelService.getChannelsByNames( + auth, + flattenArr(channelNames), + ); + return chans + ?.filter((chan) => + chan?.peers?.find((p) => p.name === name && p.namespace === namespace), + ) + ?.map((chan) => chan.name); + } + + // TODO: 优化合并? + @ResolveField(() => [String], { + nullable: true, + description: '节点加入的网络', + }) + async networks( + @Auth() auth: JwtAuth, + @Parent() ibppeer: Ibppeer, + @Loader(OrganizationLoader) + organizationLoader: DataLoader, + @Loader(FederationLoader) + fedLoader: DataLoader, + @Loader(NetworkLoader) + networkLoader: DataLoader, + ): Promise { + // org -> fed -> net -> chan -> peers => chan => net + const { namespace, name } = ibppeer; + const { federations } = await organizationLoader.load(namespace); + if (!federations || federations.length === 0) return; + const feds = await fedLoader.loadMany(federations); + const networkNames = (feds as Federation[]).map((fed) => fed?.networkNames); + if (!networkNames || networkNames.length === 0) return; + const nets = await networkLoader.loadMany(flattenArr(networkNames)); + const channelNames = (nets as Network[]).map((net) => net.channelNames); + // TODO: channelLoader + const chans = await this.channelService.getChannelsByNames( + auth, + flattenArr(channelNames), + ); + const joinedChans = chans + ?.filter((chan) => + chan?.peers?.find((p) => p.name === name && p.namespace === namespace), + ) + ?.map((chan) => chan.name); + return (nets as Network[]) + ?.filter((net) => find(net.channelNames, (o) => joinedChans.includes(o))) + ?.map((net) => net.name); + } } diff --git a/src/ibppeer/ibppeer.service.ts b/src/ibppeer/ibppeer.service.ts index c808cf0..95a56c3 100644 --- a/src/ibppeer/ibppeer.service.ts +++ b/src/ibppeer/ibppeer.service.ts @@ -35,6 +35,7 @@ export class IbppeerService { ).toISOString(), limits: ibppeer.spec?.resources?.peer?.limits, status: ibppeer.status?.type, + namespace: ibppeer.metadata?.namespace, }; } diff --git a/src/ibppeer/models/ibppeer.model.ts b/src/ibppeer/models/ibppeer.model.ts index 6b2ee9f..dce56f8 100644 --- a/src/ibppeer/models/ibppeer.model.ts +++ b/src/ibppeer/models/ibppeer.model.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Field, HideField, ID, ObjectType } from '@nestjs/graphql'; import { SpecResource } from 'src/common/models/spec-resource.model'; import { AnyObj } from 'src/types'; import { IbppeerStatus } from './ibppeer-status.enum'; @@ -11,12 +11,14 @@ export class Ibppeer { /** 创建时间 */ creationTimestamp: string; + @HideField() + namespace?: string; + /** 加入的网络 */ - // TODO + networks?: string[]; /** 加入的通道 */ - // TODO - // org -> fed -> net -> chan -> peers + channels?: string[]; /** 节点配置 */ @Field(() => SpecResource, { description: '节点配置' }) diff --git a/src/network/network.resolver.ts b/src/network/network.resolver.ts index 37a9c68..f3d6ac0 100644 --- a/src/network/network.resolver.ts +++ b/src/network/network.resolver.ts @@ -119,9 +119,7 @@ export class NetworkResolver { ): Promise { const { channelNames } = network; if (!channelNames || channelNames.length === 0) return; - return Promise.all( - channelNames.map((c) => this.channelService.getChannel(auth, c)), - ); + return this.channelService.getChannelsByNames(auth, channelNames); // TODO: list/channel 权限问题 // const cs = await channelLoader.loadMany(channelNames); // return cs; diff --git a/src/organization/models/organization.model.ts b/src/organization/models/organization.model.ts index 06cd38b..c1b45dc 100644 --- a/src/organization/models/organization.model.ts +++ b/src/organization/models/organization.model.ts @@ -49,4 +49,7 @@ export class Organization { /** 所有节点 */ ibppeers?: Ibppeer[]; + + /** 加入的通道 */ + channels?: string[]; } diff --git a/src/organization/organization.gql b/src/organization/organization.gql index 4f95858..cfc56a0 100644 --- a/src/organization/organization.gql +++ b/src/organization/organization.gql @@ -29,22 +29,16 @@ query getOrganization($name: String!) { reason networks { name - creationTimestamp - lastHeartbeatTime - expiredTime - clusterSize - organizations { - name - } } federations + channels users { name isOrganizationAdmin } ibppeers { name - creationTimestamp + creationTimestamp # TODO:去除,查询太多数据,分两个接口 status limits { cpu @@ -79,10 +73,6 @@ mutation updateOrganization($name: String!, $organization: UpdateOrganization!) admin status reason - users { - name - isOrganizationAdmin - } } } diff --git a/src/organization/organization.resolver.ts b/src/organization/organization.resolver.ts index 58f3f3b..15ba7ad 100644 --- a/src/organization/organization.resolver.ts +++ b/src/organization/organization.resolver.ts @@ -10,6 +10,7 @@ import DataLoader from 'dataloader'; import { Loader } from 'src/common/dataloader'; import { Auth } from 'src/common/decorators/auth.decorator'; import { K8sV1Status } from 'src/common/models/k8s-v1-status.model'; +import { flattenArr } from 'src/common/utils'; import { FederationLoader } from 'src/federation/federation.loader'; import { Federation } from 'src/federation/models/federation.model'; import { IbppeerService } from 'src/ibppeer/ibppeer.service'; @@ -145,4 +146,25 @@ export class OrganizationResolver { const ibppeers = await this.ibppeerService.getIbppeers(auth, name); return ibppeers; } + + @ResolveField(() => [String], { + nullable: true, + description: '组织加入的通道', + }) + async channels( + @Parent() org: Organization, + @Loader(FederationLoader) + fedLoader: DataLoader, + @Loader(NetworkLoader) + networkLoader: DataLoader, + ): Promise { + const { federations } = org; + if (!federations || federations.length === 0) return; + const feds = await fedLoader.loadMany(federations); + const networkNames = (feds as Federation[]).map((fed) => fed?.networkNames); + if (!networkNames || networkNames.length === 0) return; + const nets = await networkLoader.loadMany(flattenArr(networkNames)); + const channelNames = (nets as Network[]).map((net) => net.channelNames); + return flattenArr(channelNames); + } } diff --git a/src/schema.gql b/src/schema.gql index 02a60c2..ae6c702 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -88,6 +88,9 @@ enum FederationStatus { } type Ibppeer { + """加入的通道""" + channels: [String!] + """创建时间""" creationTimestamp: String! @@ -97,6 +100,9 @@ type Ibppeer { """name""" name: ID! + """加入的网络""" + networks: [String!] + """运行状态""" status: IbppeerStatus } @@ -357,6 +363,9 @@ type Organization { """管理员""" admin: String + """加入的通道""" + channels: [String!] + """创建时间""" creationTimestamp: String! @@ -487,6 +496,12 @@ type Query { """联盟列表""" federations: [Federation!]! + """获取组织下的节点列表""" + ibppeers( + """所在组织""" + organization: String! + ): [Ibppeer!]! + """获取「创建/更新通道」时的可选节点列表""" ibppeersForCreateChannel( """此通道的组织(包括发起者和配置成员)"""