diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 798bef1e6..78ac9e41e 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -27,7 +27,8 @@ module.exports = { }, }, rules: { - 'no-console': ['error'], + 'no-console': 'error', + camelcase: ['error', { allow: ['FederationClient_*'] }], 'no-debugger': 'error', 'prettier/prettier': [ 'error', @@ -184,6 +185,7 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.json', '**/tsconfig.json'], // this is to properly reference the referenced project database without requirement of compiling it + // eslint-disable-next-line camelcase EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, }, }, diff --git a/backend/src/federation/client/FederationClient.ts b/backend/src/federation/client/FederationClient.ts new file mode 100644 index 000000000..db1e5e3b2 --- /dev/null +++ b/backend/src/federation/client/FederationClient.ts @@ -0,0 +1,55 @@ +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' + +import { ApiVersionType } from '@/federation/enum/apiVersionType' + +import { FederationClient_1_0 } from './FederationClient_1_0' +import { FederationClient_1_1 } from './FederationClient_1_1' + +type FederationClientType = FederationClient_1_0 | FederationClient_1_1 + +interface ClientInstance { + id: number + // eslint-disable-next-line no-use-before-define + client: FederationClientType +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class FederationClient { + private static instanceArray: ClientInstance[] = [] + + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + private static createFederationClient = (dbCom: DbFederatedCommunity) => { + switch (dbCom.apiVersion) { + case ApiVersionType.V1_0: + return new FederationClient_1_0(dbCom) + case ApiVersionType.V1_1: + return new FederationClient_1_1(dbCom) + default: + return null + } + } + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static getInstance(dbCom: DbFederatedCommunity): FederationClientType | null { + const instance = FederationClient.instanceArray.find((instance) => instance.id === dbCom.id) + if (instance) { + return instance.client + } + const client = FederationClient.createFederationClient(dbCom) + if (client) { + FederationClient.instanceArray.push({ id: dbCom.id, client } as ClientInstance) + } + return client + } +} diff --git a/backend/src/federation/client/FederationClient_1_0.ts b/backend/src/federation/client/FederationClient_1_0.ts new file mode 100644 index 000000000..c8e878ded --- /dev/null +++ b/backend/src/federation/client/FederationClient_1_0.ts @@ -0,0 +1,48 @@ +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { GraphQLClient } from 'graphql-request' + +import { getPublicKey } from '@/federation/query/getPublicKey' +import { backendLogger as logger } from '@/server/logger' + +export class FederationClient_1_0 { + dbCom: DbFederatedCommunity + endpoint: string + client: GraphQLClient + + constructor(dbCom: DbFederatedCommunity) { + this.dbCom = dbCom + this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${ + dbCom.apiVersion + }/` + this.client = new GraphQLClient(this.endpoint, { + method: 'GET', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } + + getPublicKey = async (): Promise => { + logger.info('Federation: getPublicKey from endpoint', this.endpoint) + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data } = await this.client.rawRequest(getPublicKey, {}) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.getPublicKey?.publicKey) { + logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint) + return + } + logger.info( + 'Federation: getPublicKey successful from endpoint', + this.endpoint, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + data.getPublicKey.publicKey, + ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return data.getPublicKey.publicKey + } catch (err) { + logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint) + } + } +} diff --git a/backend/src/federation/client/FederationClient_1_1.ts b/backend/src/federation/client/FederationClient_1_1.ts new file mode 100644 index 000000000..27679b423 --- /dev/null +++ b/backend/src/federation/client/FederationClient_1_1.ts @@ -0,0 +1,3 @@ +import { FederationClient_1_0 } from './FederationClient_1_0' + +export class FederationClient_1_1 extends FederationClient_1_0 {} diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index 0da5914bd..16f396260 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -83,6 +83,7 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables1) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) @@ -134,6 +135,7 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables2) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) @@ -172,6 +174,7 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables3) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], })