diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 497d5d79ea..3fa3bffaba 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -19,6 +19,7 @@ import { MailModule } from './base/mailer'; import { MetricsModule } from './base/metrics'; import { MutexModule } from './base/mutex'; import { PrismaModule } from './base/prisma'; +import { RuntimeModule } from './base/runtime'; import { StorageProviderModule } from './base/storage'; import { RateLimiterModule } from './base/throttler'; import { WebSocketModule } from './base/websocket'; @@ -39,6 +40,7 @@ import { ENABLED_PLUGINS } from './plugins/registry'; export const FunctionalityModules = [ ConfigModule.forRoot(), + RuntimeModule, EventModule, CacheModule, MutexModule, @@ -74,11 +76,13 @@ function filterOptionalModule( if (nonMetRequirements.length) { const name = 'module' in module ? module.module.name : module.name; - new Logger(name).warn( - `${name} is not enabled because of the required configuration is not satisfied.`, - 'Unsatisfied configuration:', - ...nonMetRequirements.map(config => ` AFFiNE.${config}`) - ); + if (!config.node.test) { + new Logger(name).warn( + `${name} is not enabled because of the required configuration is not satisfied.`, + 'Unsatisfied configuration:', + ...nonMetRequirements.map(config => ` AFFiNE.${config}`) + ); + } return null; } } diff --git a/packages/backend/server/src/base/config/index.ts b/packages/backend/server/src/base/config/index.ts index 91afcdbf1a..7bbe2d65a8 100644 --- a/packages/backend/server/src/base/config/index.ts +++ b/packages/backend/server/src/base/config/index.ts @@ -3,7 +3,6 @@ import { merge } from 'lodash-es'; import { AFFiNEConfig } from './def'; import { Config } from './provider'; -import { Runtime } from './runtime/service'; export * from './def'; export * from './default'; @@ -17,10 +16,10 @@ function createConfigProvider( ): FactoryProvider { return { provide: Config, - useFactory: (runtime: Runtime) => { - return Object.freeze(merge({}, globalThis.AFFiNE, override, { runtime })); + useFactory: () => { + return Object.freeze(merge({}, globalThis.AFFiNE, override)); }, - inject: [Runtime], + inject: [], }; } @@ -31,10 +30,8 @@ export class ConfigModule { return { global: true, module: ConfigModule, - providers: [provider, Runtime], + providers: [provider], exports: [provider], }; }; } - -export { Runtime }; diff --git a/packages/backend/server/src/base/config/provider.ts b/packages/backend/server/src/base/config/provider.ts index abc0988d6d..5e16524d11 100644 --- a/packages/backend/server/src/base/config/provider.ts +++ b/packages/backend/server/src/base/config/provider.ts @@ -1,6 +1,5 @@ import { ApplyType } from '../utils/types'; import { AFFiNEConfig } from './def'; -import type { Runtime } from './runtime/service'; /** * @example @@ -14,6 +13,4 @@ import type { Runtime } from './runtime/service'; * } * } */ -export class Config extends ApplyType() { - runtime!: Runtime; -} +export class Config extends ApplyType() {} diff --git a/packages/backend/server/src/base/index.ts b/packages/backend/server/src/base/index.ts index a68b5f5a61..a450571b4d 100644 --- a/packages/backend/server/src/base/index.ts +++ b/packages/backend/server/src/base/index.ts @@ -30,6 +30,7 @@ export { OptionalModule, } from './nestjs'; export { type PrismaTransaction } from './prisma'; +export { Runtime } from './runtime'; export * from './storage'; export { type StorageProvider, StorageProviderFactory } from './storage'; export { CloudThrottlerGuard, SkipThrottle, Throttle } from './throttler'; diff --git a/packages/backend/server/src/base/config/runtime/event.ts b/packages/backend/server/src/base/runtime/event.ts similarity index 54% rename from packages/backend/server/src/base/config/runtime/event.ts rename to packages/backend/server/src/base/runtime/event.ts index b7964b3d21..6401c502b5 100644 --- a/packages/backend/server/src/base/config/runtime/event.ts +++ b/packages/backend/server/src/base/runtime/event.ts @@ -1,10 +1,10 @@ -import { OnEvent } from '../../event'; -import { Payload } from '../../event/def'; -import { FlattenedAppRuntimeConfig } from '../types'; +import { FlattenedAppRuntimeConfig } from '../config/types'; +import { OnEvent } from '../event'; +import { Payload } from '../event/def'; -declare module '../../event/def' { +declare module '../event/def' { interface EventDefinitions { - runtimeConfig: { + runtime: { [K in keyof FlattenedAppRuntimeConfig]: { changed: Payload; }; @@ -18,5 +18,5 @@ declare module '../../event/def' { export const OnRuntimeConfigChange_DO_NOT_USE = ( nameWithModule: keyof FlattenedAppRuntimeConfig ) => { - return OnEvent(`runtimeConfig.${nameWithModule}.changed`); + return OnEvent(`runtime.${nameWithModule}.changed`); }; diff --git a/packages/backend/server/src/base/runtime/index.ts b/packages/backend/server/src/base/runtime/index.ts new file mode 100644 index 0000000000..3e57b9184f --- /dev/null +++ b/packages/backend/server/src/base/runtime/index.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; + +import { Runtime } from './service'; + +@Global() +@Module({ + providers: [Runtime], + exports: [Runtime], +}) +export class RuntimeModule {} +export { Runtime }; diff --git a/packages/backend/server/src/base/config/runtime/service.ts b/packages/backend/server/src/base/runtime/service.ts similarity index 95% rename from packages/backend/server/src/base/config/runtime/service.ts rename to packages/backend/server/src/base/runtime/service.ts index bf043b6f34..68275f2980 100644 --- a/packages/backend/server/src/base/config/runtime/service.ts +++ b/packages/backend/server/src/base/runtime/service.ts @@ -8,11 +8,14 @@ import { import { PrismaClient } from '@prisma/client'; import { difference, keyBy } from 'lodash-es'; -import { Cache } from '../../cache'; -import { InvalidRuntimeConfigType, RuntimeConfigNotFound } from '../../error'; -import { defer } from '../../utils/promise'; -import { defaultRuntimeConfig, runtimeConfigType } from '../register'; -import { AppRuntimeConfigModules, FlattenedAppRuntimeConfig } from '../types'; +import { Cache } from '../cache'; +import { defaultRuntimeConfig, runtimeConfigType } from '../config/register'; +import { + AppRuntimeConfigModules, + FlattenedAppRuntimeConfig, +} from '../config/types'; +import { InvalidRuntimeConfigType, RuntimeConfigNotFound } from '../error'; +import { defer } from '../utils/promise'; function validateConfigType( key: K, diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index 22001ce2e4..dd954eeb9c 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -20,6 +20,7 @@ import { InternalServerError, InvalidEmail, InvalidEmailToken, + Runtime, SignUpForbidden, Throttle, URLHelper, @@ -57,7 +58,8 @@ export class AuthController { private readonly auth: AuthService, private readonly user: UserService, private readonly token: TokenService, - private readonly config: Config + private readonly config: Config, + private readonly runtime: Runtime ) { if (config.node.dev) { // set DNS servers in dev mode @@ -159,12 +161,12 @@ export class AuthController { // send email magic link const user = await this.user.findUserByEmail(email); if (!user) { - const allowSignup = await this.config.runtime.fetch('auth/allowSignup'); + const allowSignup = await this.runtime.fetch('auth/allowSignup'); if (!allowSignup) { throw new SignUpForbidden(); } - const requireEmailDomainVerification = await this.config.runtime.fetch( + const requireEmailDomainVerification = await this.runtime.fetch( 'auth/requireEmailDomainVerification' ); if (requireEmailDomainVerification) { diff --git a/packages/backend/server/src/core/config/resolver.ts b/packages/backend/server/src/core/config/resolver.ts index b68ce00471..ef64eae24c 100644 --- a/packages/backend/server/src/core/config/resolver.ts +++ b/packages/backend/server/src/core/config/resolver.ts @@ -12,7 +12,7 @@ import { import { RuntimeConfig, RuntimeConfigType } from '@prisma/client'; import { GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars'; -import { Config, URLHelper } from '../../base'; +import { Config, Runtime, URLHelper } from '../../base'; import { Public } from '../auth'; import { Admin } from '../common'; import { FeatureType } from '../features'; @@ -76,6 +76,7 @@ export class ServerFlagsType implements ServerFlags { export class ServerConfigResolver { constructor( private readonly config: Config, + private readonly runtime: Runtime, private readonly url: URLHelper, private readonly server: ServerService ) {} @@ -103,7 +104,7 @@ export class ServerConfigResolver { description: 'credentials requirement', }) async credentialsRequirement() { - const config = await this.config.runtime.fetchAll({ + const config = await this.runtime.fetchAll({ 'auth/password.max': true, 'auth/password.min': true, }); @@ -120,7 +121,7 @@ export class ServerConfigResolver { description: 'server flags', }) async flags(): Promise { - const records = await this.config.runtime.list('flags'); + const records = await this.runtime.list('flags'); return records.reduce((flags, record) => { flags[record.key as keyof ServerFlagsType] = record.value as any; @@ -184,13 +185,13 @@ interface ServerDatabaseConfig { @Admin() @Resolver(() => ServerRuntimeConfigType) export class ServerRuntimeConfigResolver { - constructor(private readonly config: Config) {} + constructor(private readonly runtime: Runtime) {} @Query(() => [ServerRuntimeConfigType], { description: 'get all server runtime configurable settings', }) serverRuntimeConfig(): Promise { - return this.config.runtime.list(); + return this.runtime.list(); } @Mutation(() => ServerRuntimeConfigType, { @@ -200,7 +201,7 @@ export class ServerRuntimeConfigResolver { @Args('id') id: string, @Args({ type: () => GraphQLJSON, name: 'value' }) value: any ): Promise { - return await this.config.runtime.set(id as any, value); + return await this.runtime.set(id as any, value); } @Mutation(() => [ServerRuntimeConfigType], { @@ -211,7 +212,7 @@ export class ServerRuntimeConfigResolver { ): Promise { const keys = Object.keys(updates); const results = await Promise.all( - keys.map(key => this.config.runtime.set(key as any, updates[key])) + keys.map(key => this.runtime.set(key as any, updates[key])) ); return results; diff --git a/packages/backend/server/src/core/doc/options.ts b/packages/backend/server/src/core/doc/options.ts index c24322156d..9f9dd1c2b3 100644 --- a/packages/backend/server/src/core/doc/options.ts +++ b/packages/backend/server/src/core/doc/options.ts @@ -7,6 +7,7 @@ import { Config, mergeUpdatesInApplyWay as yotcoMergeUpdates, metrics, + Runtime, } from '../../base'; import { PermissionService } from '../permission'; import { QuotaService } from '../quota'; @@ -35,6 +36,7 @@ export class DocStorageOptions implements IDocStorageOptions { constructor( private readonly config: Config, + private readonly runtime: Runtime, private readonly permission: PermissionService, private readonly quota: QuotaService ) {} @@ -43,9 +45,7 @@ export class DocStorageOptions implements IDocStorageOptions { const doc = await this.recoverDoc(updates); const yjsResult = Buffer.from(Y.encodeStateAsUpdate(doc)); - const useYocto = await this.config.runtime.fetch( - 'doc/experimentalMergeWithYOcto' - ); + const useYocto = await this.runtime.fetch('doc/experimentalMergeWithYOcto'); if (useYocto) { metrics.jwst.counter('codec_merge_counter').add(1); diff --git a/packages/backend/server/src/core/features/management.ts b/packages/backend/server/src/core/features/management.ts index f27d3db795..5c90b7f692 100644 --- a/packages/backend/server/src/core/features/management.ts +++ b/packages/backend/server/src/core/features/management.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { Config, type EventPayload, OnEvent } from '../../base'; +import { type EventPayload, OnEvent, Runtime } from '../../base'; import { UserService } from '../user/service'; import { FeatureService } from './service'; import { FeatureType } from './types'; @@ -19,7 +19,7 @@ export class FeatureManagementService { constructor( private readonly feature: FeatureService, private readonly user: UserService, - private readonly config: Config + private readonly runtime: Runtime ) {} // ======== Admin ======== @@ -95,7 +95,7 @@ export class FeatureManagementService { email: string, type: EarlyAccessType = EarlyAccessType.App ) { - const earlyAccessControlEnabled = await this.config.runtime.fetch( + const earlyAccessControlEnabled = await this.runtime.fetch( 'flags/earlyAccessControl' ); diff --git a/packages/backend/server/src/core/sync/gateway.ts b/packages/backend/server/src/core/sync/gateway.ts index 78b7da016d..c21b61a6f5 100644 --- a/packages/backend/server/src/core/sync/gateway.ts +++ b/packages/backend/server/src/core/sync/gateway.ts @@ -12,11 +12,11 @@ import { Socket } from 'socket.io'; import { AlreadyInSpace, CallMetric, - Config, DocNotFound, GatewayErrorWrapper, metrics, NotInSpace, + Runtime, SpaceAccessDenied, VersionRejected, } from '../../base'; @@ -139,7 +139,7 @@ export class SpaceSyncGateway private connectionCount = 0; constructor( - private readonly config: Config, + private readonly runtime: Runtime, private readonly permissions: PermissionService, private readonly workspace: PgWorkspaceDocStorageAdapter, private readonly userspace: PgUserspaceDocStorageAdapter @@ -175,7 +175,7 @@ export class SpaceSyncGateway } async assertVersion(client: Socket, version?: string) { - const shouldCheckClientVersion = await this.config.runtime.fetch( + const shouldCheckClientVersion = await this.runtime.fetch( 'flags/syncClientVersionCheck' ); if ( diff --git a/packages/backend/server/src/core/user/service.ts b/packages/backend/server/src/core/user/service.ts index 7d9325fb2e..59b6522f6f 100644 --- a/packages/backend/server/src/core/user/service.ts +++ b/packages/backend/server/src/core/user/service.ts @@ -8,6 +8,7 @@ import { EventEmitter, type EventPayload, OnEvent, + Runtime, WrongSignInCredentials, WrongSignInMethod, } from '../../base'; @@ -33,6 +34,7 @@ export class UserService { constructor( private readonly config: Config, + private readonly runtime: Runtime, private readonly crypto: CryptoHelper, private readonly prisma: PrismaClient, private readonly emitter: EventEmitter, @@ -60,7 +62,7 @@ export class UserService { validators.assertValidEmail(data.email); if (data.password) { - const config = await this.config.runtime.fetchAll({ + const config = await this.runtime.fetchAll({ 'auth/password.max': true, 'auth/password.min': true, }); @@ -242,7 +244,7 @@ export class UserService { select: Prisma.UserSelect = this.defaultUserSelect ) { if (data.password) { - const config = await this.config.runtime.fetchAll({ + const config = await this.runtime.fetchAll({ 'auth/password.max': true, 'auth/password.min': true, }); diff --git a/packages/backend/server/src/plugins/captcha/guard.ts b/packages/backend/server/src/plugins/captcha/guard.ts index 599f6060a0..8b8420fca5 100644 --- a/packages/backend/server/src/plugins/captcha/guard.ts +++ b/packages/backend/server/src/plugins/captcha/guard.ts @@ -6,9 +6,9 @@ import type { import { Injectable } from '@nestjs/common'; import { - Config, getRequestResponseFromContext, GuardProvider, + Runtime, } from '../../base'; import { CaptchaService } from './service'; @@ -21,13 +21,13 @@ export class CaptchaGuardProvider constructor( private readonly captcha: CaptchaService, - private readonly config: Config + private readonly runtime: Runtime ) { super(); } async canActivate(context: ExecutionContext) { - if (!(await this.config.runtime.fetch('plugins.captcha/enable'))) { + if (!(await this.runtime.fetch('plugins.captcha/enable'))) { return true; } diff --git a/packages/backend/server/src/plugins/payment/manager/user.ts b/packages/backend/server/src/plugins/payment/manager/user.ts index 4a196f9afb..683c6cf12b 100644 --- a/packages/backend/server/src/plugins/payment/manager/user.ts +++ b/packages/backend/server/src/plugins/payment/manager/user.ts @@ -5,10 +5,10 @@ import Stripe from 'stripe'; import { z } from 'zod'; import { - Config, EventEmitter, InternalServerError, InvalidCheckoutParameters, + Runtime, SubscriptionAlreadyExists, SubscriptionPlanNotFound, URLHelper, @@ -56,7 +56,7 @@ export class UserSubscriptionManager extends SubscriptionManager { constructor( stripe: Stripe, db: PrismaClient, - private readonly config: Config, + private readonly runtime: Runtime, private readonly feature: FeatureManagementService, private readonly event: EventEmitter, private readonly url: URLHelper @@ -617,7 +617,7 @@ export class UserSubscriptionManager extends SubscriptionManager { { proEarlyAccess, proSubscribed, onetime }: PriceStrategyStatus ) { if (lookupKey.recurring === SubscriptionRecurring.Lifetime) { - return this.config.runtime.fetch('plugins.payment/showLifetimePrice'); + return this.runtime.fetch('plugins.payment/showLifetimePrice'); } if (lookupKey.variant === SubscriptionVariant.Onetime) { diff --git a/packages/backend/server/tests/feature.spec.ts b/packages/backend/server/tests/feature.spec.ts index bbc5791f73..0e9e3192c7 100644 --- a/packages/backend/server/tests/feature.spec.ts +++ b/packages/backend/server/tests/feature.spec.ts @@ -4,7 +4,7 @@ import { INestApplication } from '@nestjs/common'; import type { TestFn } from 'ava'; import ava from 'ava'; -import { Config, ConfigModule } from '../src/base/config'; +import { Runtime } from '../src/base'; import { AuthService } from '../src/core/auth/service'; import { FeatureManagementService, @@ -26,15 +26,7 @@ const test = ava as TestFn<{ test.beforeEach(async t => { const { app } = await createTestingApp({ - imports: [ - ConfigModule.forRoot({ - server: { - host: 'example.org', - https: true, - }, - }), - FeatureModule, - ], + imports: [FeatureModule], providers: [WorkspaceResolver], tapModule: module => { module @@ -43,8 +35,8 @@ test.beforeEach(async t => { }, }); - const config = app.get(Config); - await config.runtime.set('flags/earlyAccessControl', true); + const runtime = app.get(Runtime); + await runtime.set('flags/earlyAccessControl', true); t.context.app = app; t.context.auth = app.get(AuthService); t.context.feature = app.get(FeatureService); diff --git a/packages/backend/server/tests/payment/service.spec.ts b/packages/backend/server/tests/payment/service.spec.ts index ac70f51fc3..037c243f81 100644 --- a/packages/backend/server/tests/payment/service.spec.ts +++ b/packages/backend/server/tests/payment/service.spec.ts @@ -7,8 +7,8 @@ import Sinon from 'sinon'; import Stripe from 'stripe'; import { AppModule } from '../../src/app.module'; -import { EventEmitter } from '../../src/base'; -import { Config, ConfigModule, Runtime } from '../../src/base/config'; +import { EventEmitter, Runtime } from '../../src/base'; +import { ConfigModule } from '../../src/base/config'; import { CurrentUser } from '../../src/core/auth'; import { AuthService } from '../../src/core/auth/service'; import { @@ -543,8 +543,8 @@ test('should get correct pro plan price for checking out', async t => { // any user, lifetime recurring { feature.isEarlyAccessUser.resolves(false); - const config = app.get(Config); - await config.runtime.set('plugins.payment/showLifetimePrice', true); + const runtime = app.get(Runtime); + await runtime.set('plugins.payment/showLifetimePrice', true); await service.checkout( { diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 4841c0bca8..07298ea4bd 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -9,7 +9,7 @@ import type { Response } from 'supertest'; import supertest from 'supertest'; import { AppModule, FunctionalityModules } from '../../src/app.module'; -import { Config, GlobalExceptionFilter } from '../../src/base'; +import { GlobalExceptionFilter, Runtime } from '../../src/base'; import { GqlModule } from '../../src/base/graphql'; import { AuthGuard, AuthModule } from '../../src/core/auth'; import { UserFeaturesInit1698652531198 } from '../../src/data/migrations/1698652531198-user-features-init'; @@ -111,9 +111,9 @@ export async function createTestingModule( if (init) { await m.init(); - const config = m.get(Config); + const runtime = m.get(Runtime); // by pass password min length validation - await config.runtime.set('auth/password.min', 1); + await runtime.set('auth/password.min', 1); } return m; @@ -145,9 +145,9 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { await app.init(); - const config = app.get(Config); + const runtime = app.get(Runtime); // by pass password min length validation - await config.runtime.set('auth/password.min', 1); + await runtime.set('auth/password.min', 1); return { module: m,