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

This commit is contained in:
forehalo 2024-12-13 06:27:14 +00:00
parent 4c23991047
commit 81c68032e1
No known key found for this signature in database
GPG Key ID: 56709255DC7EC728
18 changed files with 85 additions and 75 deletions

View File

@ -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;
}
}

View File

@ -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<Config> {
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 };

View File

@ -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<AFFiNEConfig>() {
runtime!: Runtime;
}
export class Config extends ApplyType<AFFiNEConfig>() {}

View File

@ -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';

View File

@ -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<FlattenedAppRuntimeConfig[K]>;
};
@ -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`);
};

View File

@ -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 };

View File

@ -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<K extends keyof FlattenedAppRuntimeConfig>(
key: K,

View File

@ -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) {

View File

@ -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<ServerFlagsType> {
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<ServerRuntimeConfigType[]> {
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<ServerRuntimeConfigType> {
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<ServerRuntimeConfigType[]> {
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;

View File

@ -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);

View File

@ -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'
);

View File

@ -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 (

View File

@ -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,
});

View File

@ -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;
}

View File

@ -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) {

View File

@ -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);

View File

@ -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(
{

View File

@ -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,