diff --git a/backend/jest.config.js b/backend/jest.config.js index f7edec3dd..de649d66e 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -16,6 +16,7 @@ module.exports = { moduleNameMapper: { '@/(.*)': '/src/$1', '@arg/(.*)': '/src/graphql/arg/$1', + '@input/(.*)': '/src/graphql/input/$1', '@dltConnector/(.*)': '/src/apis/dltConnector/$1', '@enum/(.*)': '/src/graphql/enum/$1', '@model/(.*)': '/src/graphql/model/$1', diff --git a/backend/src/graphql/arg/CommunityArgs.ts b/backend/src/graphql/arg/CommunityArgs.ts index 163a6e504..074901e06 100644 --- a/backend/src/graphql/arg/CommunityArgs.ts +++ b/backend/src/graphql/arg/CommunityArgs.ts @@ -1,14 +1,13 @@ -import { IsString } from 'class-validator' -import { Field, ArgsType, InputType } from 'type-graphql' +import { IsBoolean, IsString } from 'class-validator' +import { ArgsType, Field } from 'type-graphql' -@InputType() @ArgsType() export class CommunityArgs { - @Field(() => String) + @Field(() => String, { nullable: true }) @IsString() - uuid: string + communityIdentifier?: string | null - @Field(() => String) - @IsString() - gmsApiKey: string + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + foreign?: boolean | null } diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 5bd8b89f7..3cdb0999e 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { MaxLength, MinLength, IsString } from 'class-validator' +import { MaxLength, MinLength, IsString, IsUUID } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' diff --git a/backend/src/graphql/input/EditCommunityInput.ts b/backend/src/graphql/input/EditCommunityInput.ts new file mode 100644 index 000000000..8c74f874b --- /dev/null +++ b/backend/src/graphql/input/EditCommunityInput.ts @@ -0,0 +1,14 @@ +import { IsString, IsUUID } from 'class-validator' +import { ArgsType, Field, InputType } from 'type-graphql' + +@ArgsType() +@InputType() +export class EditCommunityInput { + @Field(() => String) + @IsUUID('4') + uuid: string + + @Field(() => String) + @IsString() + gmsApiKey: string +} diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index e720eb716..368d88a2d 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -10,6 +10,7 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql/error/GraphQLError' +import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' @@ -19,7 +20,7 @@ import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' -import { getCommunityByUuid } from './util/communities' +import { getCommunity } from './util/communities' // to do: We need a setup for the tests that closes the connection let mutate: ApolloServerTestClient['mutate'], @@ -459,7 +460,7 @@ describe('CommunityResolver', () => { await mutate({ mutation: login, variables: peterLoginData }) // HomeCommunity is still created in userFactory - homeCom = await getCommunityByUuid(admin.communityUuid) + homeCom = await getCommunity(admin.communityUuid) foreignCom1 = DbCommunity.create() foreignCom1.foreign = true @@ -478,7 +479,7 @@ describe('CommunityResolver', () => { foreignCom2.url = 'http://stage-3.gradido.net/api' foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community') foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community') - foreignCom2.communityUuid = 'Stage3-Com-UUID' + foreignCom2.communityUuid = uuidv4() foreignCom2.authenticatedAt = new Date() foreignCom2.name = 'Stage-3_Community-name' foreignCom2.description = 'Stage-3_Community-description' @@ -490,7 +491,7 @@ describe('CommunityResolver', () => { await expect( query({ query: getCommunityByUuidQuery, - variables: { communityUuid: homeCom?.communityUuid }, + variables: { communityIdentifier: homeCom?.communityUuid }, }), ).resolves.toMatchObject({ data: { @@ -563,7 +564,7 @@ describe('CommunityResolver', () => { expect( await mutate({ mutation: updateHomeCommunityQuery, - variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' }, + variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' }, }), ).toEqual( expect.objectContaining({ diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 760b982cc..6fac870b1 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,16 +1,17 @@ import { IsNull, Not } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql' +import { Resolver, Query, Authorized, Mutation, Args } from 'type-graphql' import { CommunityArgs } from '@arg//CommunityArgs' +import { EditCommunityInput } from '@input/EditCommunityInput' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { getCommunityByUuid } from './util/communities' +import { getCommunity } from './util/communities' @Resolver() export class CommunityResolver { @@ -41,41 +42,30 @@ export class CommunityResolver { return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } - @Authorized([RIGHTS.COMMUNITY_BY_UUID]) + @Authorized([RIGHTS.COMMUNITIES]) @Query(() => Community) - async community(@Arg('communityUuid') communityUuid: string): Promise { - const com: DbCommunity | null = await getCommunityByUuid(communityUuid) - if (!com) { - throw new LogError('community not found', communityUuid) + async community(@Args() { communityIdentifier, foreign }: CommunityArgs): Promise { + const community = await getCommunity(communityIdentifier, foreign) + if (!community) { + throw new LogError('community not found', communityIdentifier, foreign) } - return new Community(com) + return new Community(community) } @Authorized([RIGHTS.COMMUNITY_UPDATE]) @Mutation(() => Community) - async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise { - let homeCom: DbCommunity | null - let com: Community - if (uuid) { - let toUpdate = false - homeCom = await getCommunityByUuid(uuid) - if (!homeCom) { - throw new LogError('HomeCommunity with uuid not found: ', uuid) - } - if (homeCom.foreign) { - throw new LogError('Error: Only the HomeCommunity could be modified!') - } - if (homeCom.gmsApiKey !== gmsApiKey) { - homeCom.gmsApiKey = gmsApiKey - toUpdate = true - } - if (toUpdate) { - await DbCommunity.save(homeCom) - } - com = new Community(homeCom) - } else { - throw new LogError(`HomeCommunity without an uuid can't be modified!`) + async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise { + const homeCom = await getCommunity(uuid) + if (!homeCom) { + throw new LogError('HomeCommunity with uuid not found: ', uuid) } - return com + if (homeCom.foreign) { + throw new LogError('Error: Only the HomeCommunity could be modified!') + } + if (homeCom.gmsApiKey !== gmsApiKey) { + homeCom.gmsApiKey = gmsApiKey + await DbCommunity.save(homeCom) + } + return new Community(homeCom) } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ce1ba43da..00894ecd3 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities' +import { getCommunity, getCommunityName, isHomeCommunity } from './util/communities' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -452,7 +452,7 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - const recipCom = await getCommunityByUuid(recipientCommunityIdentifier) + const recipCom = await getCommunity(recipientCommunityIdentifier) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index e506548c5..fffe5d6b9 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -1,65 +1,116 @@ +import { FindOptionsWhere } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' +import { isURL } from 'class-validator' +import { LogError } from '@/server/LogError' +import { isUUID4 } from '@/util/validate' + +function getCommunityFindOptions( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): FindOptionsWhere { + if (communityIdentifier === undefined && foreign === undefined) { + throw new LogError('one of communityIdentifier or foreign must be set') + } + + const where: FindOptionsWhere = {} + // != null cover !== null and !== undefined + if (communityIdentifier != null) { + if (typeof communityIdentifier === 'boolean') { + if (typeof foreign === 'boolean') { + throw new LogError('communityIdentifier cannot be boolean if foreign is set separately ') + } + where.foreign = communityIdentifier + } else if (isURL(communityIdentifier)) { + where.url = communityIdentifier + } else if (isUUID4(communityIdentifier)) { + where.communityUuid = communityIdentifier + } + } + + if (typeof foreign === 'boolean') { + where.foreign = foreign + } + return where +} + +/** + * Retrieves a community from the database based on the provided identifier and foreign status. + * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. + * If communityIdentifier is a boolean, it represents the foreign status of the community. + * If foreign is provided separately and is a boolean, it represents the foreign status of the community. + * communityIdentifier and foreign cannot both be boolean + * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. + * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @returns A promise that resolves to a DbCommunity object if found, or null if not found. + */ +export async function getCommunity( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): Promise { + return DbCommunity.findOne({ where: getCommunityFindOptions(communityIdentifier, foreign) }) +} + +/** + * Retrieves a community from the database based on the provided identifier and foreign status. + * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. + * If communityIdentifier is a boolean, it represents the foreign status of the community. + * If foreign is provided separately and is a boolean, it represents the foreign status of the community. + * communityIdentifier and foreign cannot both be boolean + * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. + * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @returns A promise that resolves to a DbCommunity object if found, or throw if not found. + */ +export async function getCommunityOrFail( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): Promise { + return DbCommunity.findOneOrFail({ + where: getCommunityFindOptions(communityIdentifier, foreign), + }) +} + +/** + * Checks if a community with the given identifier exists and is not foreign. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to true if a non-foreign community exists with the given identifier, otherwise false. + */ export async function isHomeCommunity(communityIdentifier: string): Promise { - const homeCommunity = await DbCommunity.findOne({ - where: [ - { foreign: false, communityUuid: communityIdentifier }, - { foreign: false, name: communityIdentifier }, - { foreign: false, url: communityIdentifier }, - ], - }) - if (homeCommunity) { - return true - } else { - return false - } + return (await getCommunity(communityIdentifier, false)) !== null } +/** + * Retrieves the home community, i.e., a community that is not foreign. + * @returns A promise that resolves to the home community, or throw if no home community was found + */ export async function getHomeCommunity(): Promise { - return await DbCommunity.findOneOrFail({ - where: [{ foreign: false }], - }) + return getCommunityOrFail(false) } +/** + * Retrieves the URL of the community with the given identifier. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to the URL of the community or throw if no community with this identifier was found + */ export async function getCommunityUrl(communityIdentifier: string): Promise { - const community = await DbCommunity.findOneOrFail({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - return community.url + return (await getCommunityOrFail(communityIdentifier)).url } +/** + * Checks if a community with the given identifier exists and has an authenticatedAt property set. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to true if a community with an authenticatedAt property exists with the given identifier, otherwise false. + */ export async function isCommunityAuthenticated(communityIdentifier: string): Promise { - const community = await DbCommunity.findOne({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - if (community?.authenticatedAt) { - return true - } else { - return false - } + // The !! operator is used to convert the result to a boolean value. + return !!(await getCommunity(communityIdentifier))?.authenticatedAt } +/** + * Retrieves the name of the community with the given identifier. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @returns A promise that resolves to the name of the community. If the community does not exist or has no name, an empty string is returned. + */ export async function getCommunityName(communityIdentifier: string): Promise { - const community = await DbCommunity.findOne({ - where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }], - }) - if (community?.name) { - return community.name - } else { - return '' - } -} - -export async function getCommunityByUuid(communityUuid: string): Promise { - return await DbCommunity.findOne({ - where: [{ communityUuid }], - }) + return (await getCommunity(communityIdentifier))?.name ?? '' } diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 7e52327d3..435dc3d04 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm' import { Community } from '@entity/Community' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' +import { isURL } from 'class-validator' import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' +import { isEMail, isUUID4 } from '@/util/validate' import { VALID_ALIAS_REGEX } from './validateAlias' @@ -19,10 +21,11 @@ export const findUserByIdentifier = async ( communityIdentifier: string, ): Promise => { let user: DbUser | null - const communityWhere: FindOptionsWhere = - validate(communityIdentifier) && version(communityIdentifier) === 4 - ? { communityUuid: communityIdentifier } - : { name: communityIdentifier } + const communityWhere: FindOptionsWhere = isURL(communityIdentifier) + ? { url: communityIdentifier } + : isUUID4(communityIdentifier) + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } if (validate(identifier) && version(identifier) === 4) { user = await DbUser.findOne({ @@ -32,7 +35,7 @@ export const findUserByIdentifier = async ( if (!user) { throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } - } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { + } else if (isEMail(identifier)) { const userContact = await DbUserContact.findOne({ where: { email: identifier, diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 6bd106174..9e908d202 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -135,8 +135,8 @@ export const communitiesQuery = gql` ` export const getCommunityByUuidQuery = gql` - query ($communityUuid: String!) { - community(communityUuid: $communityUuid) { + query ($communityIdentifier: String!) { + community(communityIdentifier: $communityIdentifier) { id foreign name diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 4780c94e8..ab0c8a12a 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,5 +1,6 @@ import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { Decimal } from 'decimal.js-light' +import { validate, version } from 'uuid' import { Decay } from '@model/Decay' @@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean { return false } +function isUUID4(value: string): boolean { + return validate(value) && version(value) === 4 +} + +function isEMail(value: string): boolean { + return /^.{2,}@.{2,}\..{2,}$/.exec(value) !== null +} + async function calculateBalance( userId: number, amount: Decimal, @@ -42,4 +51,4 @@ async function calculateBalance( return { balance, lastTransactionId: lastTransaction.id, decay } } -export { calculateBalance, isStringBoolean } +export { calculateBalance, isStringBoolean, isUUID4, isEMail } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 28ddf1c38..c61539e12 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -49,6 +49,7 @@ "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ "@/*": ["src/*"], "@arg/*": ["src/graphql/arg/*"], + "@input/*": ["src/graphql/input/*"], "@dltConnector/*": ["src/apis/dltConnector/*"], "@enum/*": ["src/graphql/enum/*"], "@model/*": ["src/graphql/model/*"], diff --git a/dlt-connector/.env.dist b/dlt-connector/.env.dist index 1247ac3ec..6f2511c7e 100644 --- a/dlt-connector/.env.dist +++ b/dlt-connector/.env.dist @@ -19,4 +19,7 @@ DB_DATABASE_TEST=gradido_dlt_test TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=6010 \ No newline at end of file +DLT_CONNECTOR_PORT=6010 + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file diff --git a/dlt-connector/.env.template b/dlt-connector/.env.template index e3793f642..a7cbcd6ba 100644 --- a/dlt-connector/.env.template +++ b/dlt-connector/.env.template @@ -15,4 +15,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT \ No newline at end of file +DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 69bc64bb2..e8135c10d 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 66, + lines: 63, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 5bc9673de..146a096e1 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -31,6 +31,7 @@ "express": "4.17.1", "express-slow-down": "^2.0.1", "graphql": "^16.7.1", + "graphql-request": "^6.1.0", "graphql-scalars": "^1.22.2", "helmet": "^7.1.0", "log4js": "^6.7.1", diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts new file mode 100644 index 000000000..2228fdaf8 --- /dev/null +++ b/dlt-connector/src/client/BackendClient.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { gql, GraphQLClient } from 'graphql-request' + +import { CONFIG } from '@/config' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' + +const communityByForeign = gql` + query ($foreign: Boolean) { + community(foreign: $foreign) { + uuid + foreign + creationDate + } + } +` +interface Community { + community: { + uuid: string + foreign: boolean + creationDate: string + } +} +// Source: https://refactoring.guru/design-patterns/singleton/typescript/example +// and ../federation/client/FederationClientFactory.ts +/** + * A Singleton class defines the `getInstance` method that lets clients access + * the unique singleton instance. + */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class BackendClient { + // eslint-disable-next-line no-use-before-define + private static instance: BackendClient + client: GraphQLClient + /** + * 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() {} + + /** + * 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(): BackendClient | undefined { + if (!BackendClient.instance) { + BackendClient.instance = new BackendClient() + } + if (!BackendClient.instance.client) { + try { + BackendClient.instance.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } catch (e) { + logger.error("couldn't connect to backend: ", e) + return + } + } + return BackendClient.instance + } + + public async homeCommunityUUid(): Promise { + logger.info('check home community on backend') + const { data, errors } = await this.client.rawRequest(communityByForeign, { + foreign: false, + }) + if (errors) { + throw new LogError('error getting home community from backend', errors) + } + const communityDraft = new CommunityDraft() + communityDraft.uuid = data.community.uuid + communityDraft.foreign = data.community.foreign + communityDraft.createdAt = data.community.creationDate + return communityDraft + } +} diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index e6febb482..d39623c04 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -9,7 +9,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v4.2023-09-12', + EXPECTED: 'v5.2024-02-24', CURRENT: '', }, } @@ -38,6 +38,10 @@ const dltConnector = { DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010, } +const backendServer = { + BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000', +} + // Check config version constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( @@ -56,4 +60,5 @@ export const CONFIG = { ...database, ...iota, ...dltConnector, + ...backendServer, } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index c72978b35..82bdba658 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,13 +1,61 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata' + import { CONFIG } from '@/config' +import { BackendClient } from './client/BackendClient' +import { CommunityRepository } from './data/Community.repository' +import { CommunityDraft } from './graphql/input/CommunityDraft' +import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context' +import { logger } from './logging/logger' import createServer from './server/createServer' +import { LogError } from './server/LogError' + +async function waitForServer( + backend: BackendClient, + retryIntervalMs: number, + maxRetries: number, +): Promise { + let retries = 0 + while (retries < maxRetries) { + logger.info(`Attempt ${retries + 1} for connecting to backend`) + + try { + // Make a HEAD request to the server + return await backend.homeCommunityUUid() + } catch (error) { + logger.info('Server is not reachable: ', error) + } + + // Server is not reachable, wait and retry + await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)) + retries++ + } + + throw new LogError('Max retries exceeded. Server did not become reachable.') +} async function main() { // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() + // ask backend for home community if we haven't one + try { + await CommunityRepository.loadHomeCommunityKeyPair() + } catch (e) { + const backend = BackendClient.getInstance() + if (!backend) { + throw new LogError('cannot create backend client') + } + // wait for backend server to be ready + await waitForServer(backend, 1000, 8) + + const communityDraft = await backend.homeCommunityUUid() + const addCommunityContext = new AddCommunityContext(communityDraft) + await addCommunityContext.run() + } + app.listen(CONFIG.DLT_CONNECTOR_PORT, () => { // eslint-disable-next-line no-console console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`) diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index e37b2a7a0..9809d3648 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -63,7 +63,7 @@ "@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["@types", "node_modules/@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 6d50426b1..7fbe1646f 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -569,7 +569,7 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" -"@graphql-typed-document-node/core@^3.1.1": +"@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== @@ -2119,6 +2119,13 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3329,6 +3336,14 @@ graphql-query-complexity@^0.12.0: dependencies: lodash.get "^4.4.2" +graphql-request@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" + integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== + dependencies: + "@graphql-typed-document-node/core" "^3.2.0" + cross-fetch "^3.1.5" + graphql-scalars@^1.22.2: version "1.22.2" resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.22.2.tgz#6326e6fe2d0ad4228a9fea72a977e2bf26b86362" @@ -4775,7 +4790,7 @@ node-abort-controller@^3.1.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -node-fetch@^2.6.7: +node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==