refactor(server): standalone runtime module (#9120)

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