diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index c2247e93b6..2485c00082 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -4196,7 +4196,9 @@ export type WorkspacePlan = { }; export enum WorkspacePlanStatuses { + CancelationScheduled = 'cancelationScheduled', Canceled = 'canceled', + Expired = 'expired', PaymentFailed = 'paymentFailed', Trial = 'trial', Valid = 'valid' diff --git a/packages/server/assets/multiregion/typedefs/multiregion.graphql b/packages/server/assets/multiregion/typedefs/multiregion.graphql new file mode 100644 index 0000000000..bb8c9dec26 --- /dev/null +++ b/packages/server/assets/multiregion/typedefs/multiregion.graphql @@ -0,0 +1,26 @@ +type ServerRegionItem { + id: String! + key: String! + name: String! + description: String! +} + +input CreateServerRegionInput { + key: String! + name: String! + description: String! +} + +type ServerRegionMutations { + create(input: CreateServerRegionInput!): ServerRegionItem! +} + +type ServerInfoMutations { + multiRegion: ServerRegionMutations! +} + +extend type Mutation { + serverInfoMutations: ServerInfoMutations! + @hasServerRole(role: SERVER_ADMIN) + @hasScope(scope: "server:setup") +} diff --git a/packages/server/codegen.yml b/packages/server/codegen.yml index 353f09ec28..9ce3d28e9b 100644 --- a/packages/server/codegen.yml +++ b/packages/server/codegen.yml @@ -25,7 +25,6 @@ generates: User: '@/modules/core/helpers/graphTypes#UserGraphQLReturn' ActiveUserMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' UserEmailMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' - UserWorkspaceMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' ProjectMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' ProjectInviteMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' ModelMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn' diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 508929ba2a..c119e564d2 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -858,6 +858,12 @@ export type CreateModelInput = { projectId: Scalars['ID']['input']; }; +export type CreateServerRegionInput = { + description: Scalars['String']['input']; + key: Scalars['String']['input']; + name: Scalars['String']['input']; +}; + export type CreateUserEmailInput = { email: Scalars['String']['input']; }; @@ -1318,6 +1324,7 @@ export type Mutation = { /** (Re-)send the account verification e-mail */ requestVerification: Scalars['Boolean']['output']; requestVerificationByEmail: Scalars['Boolean']['output']; + serverInfoMutations: ServerInfoMutations; serverInfoUpdate?: Maybe; /** Note: The required scope to invoke this is not given out to app or personal access tokens */ serverInviteBatchCreate: Scalars['Boolean']['output']; @@ -2830,6 +2837,11 @@ export type ServerInfo = { workspaces: ServerWorkspacesInfo; }; +export type ServerInfoMutations = { + __typename?: 'ServerInfoMutations'; + multiRegion: ServerRegionMutations; +}; + export type ServerInfoUpdateInput = { adminContact?: InputMaybe; company?: InputMaybe; @@ -2860,6 +2872,24 @@ export type ServerMigration = { movedTo?: Maybe; }; +export type ServerRegionItem = { + __typename?: 'ServerRegionItem'; + description: Scalars['String']['output']; + id: Scalars['String']['output']; + key: Scalars['String']['output']; + name: Scalars['String']['output']; +}; + +export type ServerRegionMutations = { + __typename?: 'ServerRegionMutations'; + create: ServerRegionItem; +}; + + +export type ServerRegionMutationsCreateArgs = { + input: CreateServerRegionInput; +}; + export enum ServerRole { ServerAdmin = 'SERVER_ADMIN', ServerArchivedUser = 'SERVER_ARCHIVED_USER', @@ -4216,7 +4246,9 @@ export type WorkspacePlan = { }; export enum WorkspacePlanStatuses { + CancelationScheduled = 'cancelationScheduled', Canceled = 'canceled', + Expired = 'expired', PaymentFailed = 'paymentFailed', Trial = 'trial', Valid = 'valid' @@ -4469,6 +4501,7 @@ export type ResolversTypes = { CreateCommentInput: CreateCommentInput; CreateCommentReplyInput: CreateCommentReplyInput; CreateModelInput: CreateModelInput; + CreateServerRegionInput: CreateServerRegionInput; CreateUserEmailInput: CreateUserEmailInput; CreateVersionInput: CreateVersionInput; Currency: Currency; @@ -4564,10 +4597,13 @@ export type ResolversTypes = { ServerAutomateInfo: ResolverTypeWrapper; ServerConfiguration: ResolverTypeWrapper; ServerInfo: ResolverTypeWrapper; + ServerInfoMutations: ResolverTypeWrapper; ServerInfoUpdateInput: ServerInfoUpdateInput; ServerInvite: ResolverTypeWrapper; ServerInviteCreateInput: ServerInviteCreateInput; ServerMigration: ResolverTypeWrapper; + ServerRegionItem: ResolverTypeWrapper; + ServerRegionMutations: ResolverTypeWrapper; ServerRole: ServerRole; ServerRoleItem: ResolverTypeWrapper; ServerStatistics: ResolverTypeWrapper; @@ -4739,6 +4775,7 @@ export type ResolversParentTypes = { CreateCommentInput: CreateCommentInput; CreateCommentReplyInput: CreateCommentReplyInput; CreateModelInput: CreateModelInput; + CreateServerRegionInput: CreateServerRegionInput; CreateUserEmailInput: CreateUserEmailInput; CreateVersionInput: CreateVersionInput; DateTime: Scalars['DateTime']['output']; @@ -4820,10 +4857,13 @@ export type ResolversParentTypes = { ServerAutomateInfo: ServerAutomateInfo; ServerConfiguration: ServerConfiguration; ServerInfo: ServerInfoGraphQLReturn; + ServerInfoMutations: ServerInfoMutations; ServerInfoUpdateInput: ServerInfoUpdateInput; ServerInvite: ServerInviteGraphQLReturnType; ServerInviteCreateInput: ServerInviteCreateInput; ServerMigration: ServerMigration; + ServerRegionItem: ServerRegionItem; + ServerRegionMutations: ServerRegionMutations; ServerRoleItem: ServerRoleItem; ServerStatistics: GraphQLEmptyReturn; ServerStats: GraphQLEmptyReturn; @@ -5517,6 +5557,7 @@ export type MutationResolvers; requestVerification?: Resolver; requestVerificationByEmail?: Resolver>; + serverInfoMutations?: Resolver; serverInfoUpdate?: Resolver, ParentType, ContextType, RequireFields>; serverInviteBatchCreate?: Resolver>; serverInviteCreate?: Resolver>; @@ -5914,6 +5955,11 @@ export type ServerInfoResolvers; }; +export type ServerInfoMutationsResolvers = { + multiRegion?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ServerInviteResolvers = { email?: Resolver; id?: Resolver; @@ -5927,6 +5973,19 @@ export type ServerMigrationResolvers; }; +export type ServerRegionItemResolvers = { + description?: Resolver; + id?: Resolver; + key?: Resolver; + name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ServerRegionMutationsResolvers = { + create?: Resolver>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ServerRoleItemResolvers = { id?: Resolver; title?: Resolver; @@ -6497,8 +6556,11 @@ export type Resolvers = { ServerAutomateInfo?: ServerAutomateInfoResolvers; ServerConfiguration?: ServerConfigurationResolvers; ServerInfo?: ServerInfoResolvers; + ServerInfoMutations?: ServerInfoMutationsResolvers; ServerInvite?: ServerInviteResolvers; ServerMigration?: ServerMigrationResolvers; + ServerRegionItem?: ServerRegionItemResolvers; + ServerRegionMutations?: ServerRegionMutationsResolvers; ServerRoleItem?: ServerRoleItemResolvers; ServerStatistics?: ServerStatisticsResolvers; ServerStats?: ServerStatsResolvers; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 5ccc912cfc..b2d1bb26bd 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -841,6 +841,12 @@ export type CreateModelInput = { projectId: Scalars['ID']['input']; }; +export type CreateServerRegionInput = { + description: Scalars['String']['input']; + key: Scalars['String']['input']; + name: Scalars['String']['input']; +}; + export type CreateUserEmailInput = { email: Scalars['String']['input']; }; @@ -1301,6 +1307,7 @@ export type Mutation = { /** (Re-)send the account verification e-mail */ requestVerification: Scalars['Boolean']['output']; requestVerificationByEmail: Scalars['Boolean']['output']; + serverInfoMutations: ServerInfoMutations; serverInfoUpdate?: Maybe; /** Note: The required scope to invoke this is not given out to app or personal access tokens */ serverInviteBatchCreate: Scalars['Boolean']['output']; @@ -2813,6 +2820,11 @@ export type ServerInfo = { workspaces: ServerWorkspacesInfo; }; +export type ServerInfoMutations = { + __typename?: 'ServerInfoMutations'; + multiRegion: ServerRegionMutations; +}; + export type ServerInfoUpdateInput = { adminContact?: InputMaybe; company?: InputMaybe; @@ -2843,6 +2855,24 @@ export type ServerMigration = { movedTo?: Maybe; }; +export type ServerRegionItem = { + __typename?: 'ServerRegionItem'; + description: Scalars['String']['output']; + id: Scalars['String']['output']; + key: Scalars['String']['output']; + name: Scalars['String']['output']; +}; + +export type ServerRegionMutations = { + __typename?: 'ServerRegionMutations'; + create: ServerRegionItem; +}; + + +export type ServerRegionMutationsCreateArgs = { + input: CreateServerRegionInput; +}; + export enum ServerRole { ServerAdmin = 'SERVER_ADMIN', ServerArchivedUser = 'SERVER_ARCHIVED_USER', @@ -4199,7 +4229,9 @@ export type WorkspacePlan = { }; export enum WorkspacePlanStatuses { + CancelationScheduled = 'cancelationScheduled', Canceled = 'canceled', + Expired = 'expired', PaymentFailed = 'paymentFailed', Trial = 'trial', Valid = 'valid' diff --git a/packages/server/modules/index.ts b/packages/server/modules/index.ts index 75c18d99d3..1d5a2b3255 100644 --- a/packages/server/modules/index.ts +++ b/packages/server/modules/index.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-var-requires */ + import fs from 'fs' import path from 'path' import { appRoot, packageRoot } from '@/bootstrap' @@ -22,6 +22,7 @@ import { } from '@/modules/core/graph/helpers/directiveHelper' import { AppMocksConfig } from '@/modules/mocks' import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks' +import { LogicError } from '@/modules/shared/errors' /** * Cached speckle module requires @@ -77,7 +78,8 @@ const getEnabledModuleNames = () => { 'serverinvites', 'stats', 'webhooks', - 'workspacesCore' + 'workspacesCore', + 'multiregion' ] if (FF_AUTOMATE_MODULE_ENABLED) moduleNames.push('automate') @@ -93,7 +95,13 @@ async function getSpeckleModules() { const moduleNames = getEnabledModuleNames() for (const dir of moduleNames) { - loadedModules.push(require(`./${dir}`)) + const moduleIndex = await import(`./${dir}/index`) + const moduleDefinition = 'init' in moduleIndex ? moduleIndex : moduleIndex.default + if (!('init' in moduleDefinition)) { + throw new LogicError(`Module ${dir} does not have an init function`) + } + + loadedModules.push(moduleDefinition) } return loadedModules @@ -185,7 +193,10 @@ const graphComponents = (): Pick, 'resolvers'> & { // first pass load of resolvers const resolversPath = path.join(fullPath, 'graph', 'resolvers') if (fs.existsSync(resolversPath)) { - resolverObjs = [...resolverObjs, ...values(autoloadFromDirectory(resolversPath))] + const newResolverObjs = values(autoloadFromDirectory(resolversPath)).map((o) => + 'default' in o ? o.default : o + ) + resolverObjs = [...resolverObjs, ...newResolverObjs] } // load directives diff --git a/packages/server/modules/multiregion/domain/operations.ts b/packages/server/modules/multiregion/domain/operations.ts new file mode 100644 index 0000000000..d54e100669 --- /dev/null +++ b/packages/server/modules/multiregion/domain/operations.ts @@ -0,0 +1,5 @@ +import { RegionServerConfig } from '@/modules/multiregion/domain/types' + +export type GetAvailableRegionConfigs = () => Promise<{ + [key: string]: RegionServerConfig +}> diff --git a/packages/server/modules/multiregion/domain/types.ts b/packages/server/modules/multiregion/domain/types.ts new file mode 100644 index 0000000000..0360640595 --- /dev/null +++ b/packages/server/modules/multiregion/domain/types.ts @@ -0,0 +1,12 @@ +export type RegionServerConfig = { + postgres: { + /** + * Full Postgres connection URI (e.g. "postgres://user:password@host:port/dbname") + */ + connectionUri: string + /** + * SSL cert, if any + */ + publicTlsCertificate?: string + } +} diff --git a/packages/server/modules/multiregion/errors/index.ts b/packages/server/modules/multiregion/errors/index.ts new file mode 100644 index 0000000000..8d419e6c3e --- /dev/null +++ b/packages/server/modules/multiregion/errors/index.ts @@ -0,0 +1,6 @@ +import { BaseError } from '@/modules/shared/errors' + +export class MultiRegionSupportDisabledError extends BaseError { + static code = 'MULTI_REGION_SUPPORT_DISABLED' + static defaultMessage = 'Multi region support is disabled' +} diff --git a/packages/server/modules/multiregion/graph/mocks/index.ts b/packages/server/modules/multiregion/graph/mocks/index.ts new file mode 100644 index 0000000000..2259af88c3 --- /dev/null +++ b/packages/server/modules/multiregion/graph/mocks/index.ts @@ -0,0 +1,16 @@ +import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks' +import { faker } from '@faker-js/faker' + +export default { + mocks: { + ServerRegionItem: () => { + const key = faker.string.uuid() + return { + id: key, + key, + name: faker.address.country(), + description: faker.lorem.sentence() + } + } + } +} as SpeckleModuleMocksConfig diff --git a/packages/server/modules/multiregion/graph/resolvers/index.ts b/packages/server/modules/multiregion/graph/resolvers/index.ts new file mode 100644 index 0000000000..eb350330a9 --- /dev/null +++ b/packages/server/modules/multiregion/graph/resolvers/index.ts @@ -0,0 +1,5 @@ +import { Resolvers } from '@/modules/core/graph/generated/graphql' + +// TODO: Real implementation? Need to discuss questions regarding data model & region config first + +export default {} as Resolvers diff --git a/packages/server/modules/multiregion/helpers/index.ts b/packages/server/modules/multiregion/helpers/index.ts new file mode 100644 index 0000000000..b04c4ae48e --- /dev/null +++ b/packages/server/modules/multiregion/helpers/index.ts @@ -0,0 +1,7 @@ +import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' + +export const isMultiRegionEnabled = () => { + const { FF_WORKSPACES_MODULE_ENABLED, FF_WORKSPACES_MULTI_REGION_ENABLED } = + getFeatureFlags() + return !!(FF_WORKSPACES_MODULE_ENABLED && FF_WORKSPACES_MULTI_REGION_ENABLED) +} diff --git a/packages/server/modules/multiregion/index.ts b/packages/server/modules/multiregion/index.ts new file mode 100644 index 0000000000..03167fc0f7 --- /dev/null +++ b/packages/server/modules/multiregion/index.ts @@ -0,0 +1,14 @@ +import { moduleLogger } from '@/logging/logging' +import { isMultiRegionEnabled } from '@/modules/multiregion/helpers' +import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' + +const multiregion: SpeckleModule = { + init() { + const isEnabled = isMultiRegionEnabled() + if (isEnabled) { + moduleLogger.info('🌍 Init multiregion module') + } + } +} + +export default multiregion diff --git a/packages/server/modules/multiregion/services/config.ts b/packages/server/modules/multiregion/services/config.ts new file mode 100644 index 0000000000..13c9eb5e97 --- /dev/null +++ b/packages/server/modules/multiregion/services/config.ts @@ -0,0 +1,14 @@ +import { GetAvailableRegionConfigs } from '@/modules/multiregion/domain/operations' + +export const getAvailableRegionConfigsFactory = + (): GetAvailableRegionConfigs => async () => { + // TODO: Hardcoded for now, should be fetched from a config file + return { + eu: { + postgres: { + connectionUri: 'postgresql://speckle:speckle@localhost/speckle_eu', + publicTlsCertificate: undefined + } + } + } + } diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 48bec9e37b..c6815556e8 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -842,6 +842,12 @@ export type CreateModelInput = { projectId: Scalars['ID']['input']; }; +export type CreateServerRegionInput = { + description: Scalars['String']['input']; + key: Scalars['String']['input']; + name: Scalars['String']['input']; +}; + export type CreateUserEmailInput = { email: Scalars['String']['input']; }; @@ -1302,6 +1308,7 @@ export type Mutation = { /** (Re-)send the account verification e-mail */ requestVerification: Scalars['Boolean']['output']; requestVerificationByEmail: Scalars['Boolean']['output']; + serverInfoMutations: ServerInfoMutations; serverInfoUpdate?: Maybe; /** Note: The required scope to invoke this is not given out to app or personal access tokens */ serverInviteBatchCreate: Scalars['Boolean']['output']; @@ -2814,6 +2821,11 @@ export type ServerInfo = { workspaces: ServerWorkspacesInfo; }; +export type ServerInfoMutations = { + __typename?: 'ServerInfoMutations'; + multiRegion: ServerRegionMutations; +}; + export type ServerInfoUpdateInput = { adminContact?: InputMaybe; company?: InputMaybe; @@ -2844,6 +2856,24 @@ export type ServerMigration = { movedTo?: Maybe; }; +export type ServerRegionItem = { + __typename?: 'ServerRegionItem'; + description: Scalars['String']['output']; + id: Scalars['String']['output']; + key: Scalars['String']['output']; + name: Scalars['String']['output']; +}; + +export type ServerRegionMutations = { + __typename?: 'ServerRegionMutations'; + create: ServerRegionItem; +}; + + +export type ServerRegionMutationsCreateArgs = { + input: CreateServerRegionInput; +}; + export enum ServerRole { ServerAdmin = 'SERVER_ADMIN', ServerArchivedUser = 'SERVER_ARCHIVED_USER', @@ -4200,7 +4230,9 @@ export type WorkspacePlan = { }; export enum WorkspacePlanStatuses { + CancelationScheduled = 'cancelationScheduled', Canceled = 'canceled', + Expired = 'expired', PaymentFailed = 'paymentFailed', Trial = 'trial', Valid = 'valid' diff --git a/packages/shared/src/environment/index.ts b/packages/shared/src/environment/index.ts index e7a228849c..1ccd39db97 100644 --- a/packages/shared/src/environment/index.ts +++ b/packages/shared/src/environment/index.ts @@ -42,6 +42,11 @@ function parseFeatureFlags() { FF_NO_CLOSURE_WRITES: { schema: z.boolean(), defaults: { production: false, _: false } + }, + // Enables workspaces multi region DB support + FF_WORKSPACES_MULTI_REGION_ENABLED: { + schema: z.boolean(), + defaults: { production: false, _: true } } }) } @@ -56,6 +61,7 @@ export function getFeatureFlags(): { FF_WORKSPACES_SSO_ENABLED: boolean FF_GATEKEEPER_MODULE_ENABLED: boolean FF_BILLING_INTEGRATION_ENABLED: boolean + FF_WORKSPACES_MULTI_REGION_ENABLED: boolean } { if (!parsedFlags) parsedFlags = parseFeatureFlags() return parsedFlags