refactor(server): plugin modules (#5630)
- [x] separates modules into `fundamental`, `core`, `plugins`
- [x] optional modules with `@OptionalModule` decorator to install modules with requirements met(`requires`, `if`)
- [x] `module.contributesTo` defines optional features that will be enabled if module registered
- [x] `AFFiNE.plugins.use('payment', {})` to enable a optional/plugin module
- [x] `PaymentModule` is the first plugin module
- [x] GraphQLSchema will not be generated for non-included modules
- [x] Frontend can use `ServerConfigType` query to detect which features are enabled
- [x] override existing provider globally
This commit is contained in:
130 files changed
+1297
-974
No files matched your search
@@ -19,6 +19,7 @@ env:
|
|||||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
|
||||||
|
DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|||||||
@@ -1,39 +1,169 @@
|
|||||||
import { DynamicModule, Module, Type } from '@nestjs/common';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
import { Logger, Module } from '@nestjs/common';
|
||||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
|
import { AuthModule } from './core/auth';
|
||||||
|
import { ADD_ENABLED_FEATURES, ServerConfigModule } from './core/config';
|
||||||
|
import { DocModule } from './core/doc';
|
||||||
|
import { FeatureModule } from './core/features';
|
||||||
|
import { QuotaModule } from './core/quota';
|
||||||
|
import { StorageModule } from './core/storage';
|
||||||
|
import { SyncModule } from './core/sync';
|
||||||
|
import { UsersModule } from './core/users';
|
||||||
|
import { WorkspaceModule } from './core/workspaces';
|
||||||
|
import { getOptionalModuleMetadata } from './fundamentals';
|
||||||
import { CacheInterceptor, CacheModule } from './fundamentals/cache';
|
import { CacheInterceptor, CacheModule } from './fundamentals/cache';
|
||||||
import { ConfigModule } from './fundamentals/config';
|
import {
|
||||||
|
type AvailablePlugins,
|
||||||
|
Config,
|
||||||
|
ConfigModule,
|
||||||
|
} from './fundamentals/config';
|
||||||
import { EventModule } from './fundamentals/event';
|
import { EventModule } from './fundamentals/event';
|
||||||
|
import { GqlModule } from './fundamentals/graphql';
|
||||||
import { MailModule } from './fundamentals/mailer';
|
import { MailModule } from './fundamentals/mailer';
|
||||||
import { MetricsModule } from './fundamentals/metrics';
|
import { MetricsModule } from './fundamentals/metrics';
|
||||||
import { PrismaModule } from './fundamentals/prisma';
|
import { PrismaModule } from './fundamentals/prisma';
|
||||||
import { SessionModule } from './fundamentals/session';
|
import { SessionModule } from './fundamentals/session';
|
||||||
import { RateLimiterModule } from './fundamentals/throttler';
|
import { RateLimiterModule } from './fundamentals/throttler';
|
||||||
import { BusinessModules } from './modules';
|
import { WebSocketModule } from './fundamentals/websocket';
|
||||||
import { AuthModule } from './modules/auth';
|
import { pluginsMap } from './plugins';
|
||||||
|
|
||||||
export const FunctionalityModules: Array<Type | DynamicModule> = [
|
export const FunctionalityModules = [
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
EventModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
MetricsModule,
|
MetricsModule,
|
||||||
EventModule,
|
|
||||||
SessionModule,
|
|
||||||
RateLimiterModule,
|
RateLimiterModule,
|
||||||
AuthModule,
|
SessionModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
export class AppModuleBuilder {
|
||||||
providers: [
|
private readonly modules: AFFiNEModule[] = [];
|
||||||
{
|
constructor(private readonly config: Config) {}
|
||||||
provide: APP_INTERCEPTOR,
|
|
||||||
useClass: CacheInterceptor,
|
use(...modules: AFFiNEModule[]): this {
|
||||||
},
|
modules.forEach(m => {
|
||||||
],
|
const requirements = getOptionalModuleMetadata(m, 'requires');
|
||||||
imports: [...FunctionalityModules, ...BusinessModules],
|
// if condition not set or condition met, include the module
|
||||||
controllers:
|
if (requirements?.length) {
|
||||||
process.env.SERVER_FLAVOR === 'selfhosted' ? [] : [AppController],
|
const nonMetRequirements = requirements.filter(c => {
|
||||||
})
|
const value = get(this.config, c);
|
||||||
export class AppModule {}
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
(typeof value === 'string' && value.trim().length === 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nonMetRequirements.length) {
|
||||||
|
const name = 'module' in m ? m.module.name : m.name;
|
||||||
|
new Logger(name).warn(
|
||||||
|
`${name} is not enabled because of the required configuration is not satisfied.`,
|
||||||
|
'Unsatisfied configuration:',
|
||||||
|
...nonMetRequirements.map(config => ` AFFiNE.${config}`)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const predicator = getOptionalModuleMetadata(m, 'if');
|
||||||
|
if (predicator && !predicator(this.config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contribution = getOptionalModuleMetadata(m, 'contributesTo');
|
||||||
|
if (contribution) {
|
||||||
|
ADD_ENABLED_FEATURES(contribution);
|
||||||
|
}
|
||||||
|
this.modules.push(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
useIf(
|
||||||
|
predicator: (config: Config) => boolean,
|
||||||
|
...modules: AFFiNEModule[]
|
||||||
|
): this {
|
||||||
|
if (predicator(this.config)) {
|
||||||
|
this.use(...modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
compile() {
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: CacheInterceptor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imports: this.modules,
|
||||||
|
controllers: this.config.flavor.selfhosted ? [] : [AppController],
|
||||||
|
})
|
||||||
|
class AppModule {}
|
||||||
|
|
||||||
|
return AppModule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAppModule() {
|
||||||
|
const factor = new AppModuleBuilder(AFFiNE);
|
||||||
|
|
||||||
|
factor
|
||||||
|
// common fundamental modules
|
||||||
|
.use(...FunctionalityModules)
|
||||||
|
// auth
|
||||||
|
.use(AuthModule)
|
||||||
|
|
||||||
|
// business modules
|
||||||
|
.use(DocModule)
|
||||||
|
|
||||||
|
// sync server only
|
||||||
|
.useIf(config => config.flavor.sync, SyncModule)
|
||||||
|
|
||||||
|
// main server only
|
||||||
|
.useIf(
|
||||||
|
config => config.flavor.main,
|
||||||
|
ServerConfigModule,
|
||||||
|
WebSocketModule,
|
||||||
|
GqlModule,
|
||||||
|
StorageModule,
|
||||||
|
UsersModule,
|
||||||
|
WorkspaceModule,
|
||||||
|
FeatureModule,
|
||||||
|
QuotaModule
|
||||||
|
)
|
||||||
|
|
||||||
|
// self hosted server only
|
||||||
|
.useIf(
|
||||||
|
config => config.flavor.selfhosted,
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: join('/app', 'static'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// plugin modules
|
||||||
|
AFFiNE.plugins.enabled.forEach(name => {
|
||||||
|
const plugin = pluginsMap.get(name as AvailablePlugins);
|
||||||
|
if (!plugin) {
|
||||||
|
throw new Error(`Unknown plugin ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
factor.use(plugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
return factor.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppModule = buildAppModule();
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { Type } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||||
|
|
||||||
import { CacheRedis } from './fundamentals/cache/redis';
|
import { SocketIoAdapter } from './fundamentals';
|
||||||
import { RedisIoAdapter } from './fundamentals/websocket';
|
import { SocketIoAdapterImpl } from './fundamentals/websocket';
|
||||||
import { ExceptionLogger } from './middleware/exception-logger';
|
import { ExceptionLogger } from './middleware/exception-logger';
|
||||||
import { serverTimingAndCache } from './middleware/timing';
|
import { serverTimingAndCache } from './middleware/timing';
|
||||||
|
|
||||||
@@ -31,11 +32,16 @@ export async function createApp() {
|
|||||||
app.useGlobalFilters(new ExceptionLogger());
|
app.useGlobalFilters(new ExceptionLogger());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
if (AFFiNE.redis.enabled) {
|
if (AFFiNE.flavor.sync) {
|
||||||
const redis = app.get(CacheRedis, { strict: false });
|
const SocketIoAdapter = app.get<Type<SocketIoAdapter>>(
|
||||||
const redisIoAdapter = new RedisIoAdapter(app);
|
SocketIoAdapterImpl,
|
||||||
await redisIoAdapter.connectToRedis(redis);
|
{
|
||||||
app.useWebSocketAdapter(redisIoAdapter);
|
strict: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const adapter = new SocketIoAdapter(app);
|
||||||
|
app.useWebSocketAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -21,20 +21,19 @@ AFFiNE.ENV_MAP = {
|
|||||||
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
|
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
|
||||||
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
|
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
|
||||||
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
|
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
|
||||||
REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'],
|
REDIS_SERVER_HOST: 'plugins.redis.host',
|
||||||
REDIS_SERVER_HOST: 'redis.host',
|
REDIS_SERVER_PORT: ['plugins.redis.port', 'int'],
|
||||||
REDIS_SERVER_PORT: ['redis.port', 'int'],
|
REDIS_SERVER_USER: 'plugins.redis.username',
|
||||||
REDIS_SERVER_USER: 'redis.username',
|
REDIS_SERVER_PASSWORD: 'plugins.redis.password',
|
||||||
REDIS_SERVER_PASSWORD: 'redis.password',
|
REDIS_SERVER_DATABASE: ['plugins.redis.db', 'int'],
|
||||||
REDIS_SERVER_DATABASE: ['redis.database', 'int'],
|
|
||||||
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
|
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
|
||||||
DOC_MERGE_USE_JWST_CODEC: [
|
DOC_MERGE_USE_JWST_CODEC: [
|
||||||
'doc.manager.experimentalMergeWithJwstCodec',
|
'doc.manager.experimentalMergeWithJwstCodec',
|
||||||
'boolean',
|
'boolean',
|
||||||
],
|
],
|
||||||
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
|
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
|
||||||
STRIPE_API_KEY: 'payment.stripe.keys.APIKey',
|
STRIPE_API_KEY: 'plugins.payment.stripe.keys.APIKey',
|
||||||
STRIPE_WEBHOOK_KEY: 'payment.stripe.keys.webhookKey',
|
STRIPE_WEBHOOK_KEY: 'plugins.payment.stripe.keys.webhookKey',
|
||||||
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
|
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,38 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
// Custom configurations
|
// Custom configurations
|
||||||
const env = process.env;
|
const env = process.env;
|
||||||
const node = AFFiNE.node;
|
|
||||||
|
|
||||||
// TODO(@forehalo): detail explained
|
// TODO(@forehalo): detail explained
|
||||||
if (node.prod) {
|
// Storage
|
||||||
// Storage
|
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
|
||||||
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
|
AFFiNE.storage.providers.r2 = {
|
||||||
AFFiNE.storage.providers.r2 = {
|
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
|
||||||
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
|
credentials: {
|
||||||
credentials: {
|
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
|
||||||
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
|
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
|
||||||
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
|
},
|
||||||
},
|
};
|
||||||
};
|
AFFiNE.storage.storages.avatar.provider = 'r2';
|
||||||
AFFiNE.storage.storages.avatar.provider = 'r2';
|
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
|
||||||
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
|
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
|
||||||
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
|
`https://avatar.affineassets.com/${key}`;
|
||||||
`https://avatar.affineassets.com/${key}`;
|
|
||||||
|
|
||||||
AFFiNE.storage.storages.blob.provider = 'r2';
|
AFFiNE.storage.storages.blob.provider = 'r2';
|
||||||
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
|
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
|
||||||
AFFiNE.affine.canary ? 'canary' : 'prod'
|
AFFiNE.affine.canary ? 'canary' : 'prod'
|
||||||
}`;
|
}`;
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics
|
|
||||||
AFFiNE.metrics.enabled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
AFFiNE.metrics.enabled = true;
|
||||||
|
|
||||||
|
// Plugins Section Start
|
||||||
|
AFFiNE.plugins.use('payment', {
|
||||||
|
stripe: {
|
||||||
|
apiVersion: '2023-10-16',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
AFFiNE.plugins.use('redis');
|
||||||
|
// Plugins Section end
|
||||||
|
|
||||||
export default AFFiNE;
|
export default AFFiNE;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,58 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { Field, ObjectType, Query, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
export enum ServerFeature {
|
||||||
|
Payment = 'payment',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(ServerFeature, {
|
||||||
|
name: 'ServerFeature',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ENABLED_FEATURES: ServerFeature[] = [];
|
||||||
|
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
|
||||||
|
ENABLED_FEATURES.push(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ServerConfigType {
|
||||||
|
@Field({
|
||||||
|
description:
|
||||||
|
'server identical name could be shown as badge on user interface',
|
||||||
|
})
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Field({ description: 'server version' })
|
||||||
|
version!: string;
|
||||||
|
|
||||||
|
@Field({ description: 'server base url' })
|
||||||
|
baseUrl!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
|
||||||
|
flavor!: string;
|
||||||
|
|
||||||
|
@Field(() => [ServerFeature], { description: 'enabled server features' })
|
||||||
|
features!: ServerFeature[];
|
||||||
|
}
|
||||||
|
export class ServerConfigResolver {
|
||||||
|
@Query(() => ServerConfigType, {
|
||||||
|
description: 'server config',
|
||||||
|
})
|
||||||
|
serverConfig(): ServerConfigType {
|
||||||
|
return {
|
||||||
|
name: AFFiNE.serverName,
|
||||||
|
version: AFFiNE.version,
|
||||||
|
baseUrl: AFFiNE.baseUrl,
|
||||||
|
flavor: AFFiNE.flavor.type,
|
||||||
|
features: ENABLED_FEATURES,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ServerConfigResolver],
|
||||||
|
})
|
||||||
|
export class ServerConfigModule {}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+23
-1
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { PrismaService } from '../../fundamentals';
|
import { type EventPayload, OnEvent, PrismaService } from '../../fundamentals';
|
||||||
import { FeatureKind } from '../features';
|
import { FeatureKind } from '../features';
|
||||||
import { QuotaConfig } from './quota';
|
import { QuotaConfig } from './quota';
|
||||||
import { QuotaType } from './types';
|
import { QuotaType } from './types';
|
||||||
@@ -155,4 +155,26 @@ export class QuotaService {
|
|||||||
})
|
})
|
||||||
.then(count => count > 0);
|
.then(count => count > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.subscription.activated')
|
||||||
|
async onSubscriptionUpdated({
|
||||||
|
userId,
|
||||||
|
}: EventPayload<'user.subscription.activated'>) {
|
||||||
|
await this.switchUserQuota(
|
||||||
|
userId,
|
||||||
|
QuotaType.ProPlanV1,
|
||||||
|
'subscription activated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.subscription.canceled')
|
||||||
|
async onSubscriptionCanceled(
|
||||||
|
userId: EventPayload<'user.subscription.canceled'>
|
||||||
|
) {
|
||||||
|
await this.switchUserQuota(
|
||||||
|
userId,
|
||||||
|
QuotaType.FreePlanV1,
|
||||||
|
'subscription canceled'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -13,6 +13,9 @@ import { RevertCommand, RunCommand } from './commands/run';
|
|||||||
enableUpdateAutoMerging: false,
|
enableUpdateAutoMerging: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metrics: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
BusinessAppModule,
|
BusinessAppModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class SelfHostAdmin1605053000403 {
|
|||||||
// do the migration
|
// do the migration
|
||||||
static async up(db: PrismaClient, ref: ModuleRef) {
|
static async up(db: PrismaClient, ref: ModuleRef) {
|
||||||
const config = ref.get(Config, { strict: false });
|
const config = ref.get(Config, { strict: false });
|
||||||
if (config.flavor === 'selfhosted') {
|
if (config.flavor.selfhosted) {
|
||||||
if (
|
if (
|
||||||
!process.env.AFFINE_ADMIN_EMAIL ||
|
!process.env.AFFINE_ADMIN_EMAIL ||
|
||||||
!process.env.AFFINE_ADMIN_PASSWORD
|
!process.env.AFFINE_ADMIN_PASSWORD
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { DocID } from '../../modules/utils/doc';
|
import { DocID } from '../../core/utils/doc';
|
||||||
|
|
||||||
export class Guid1698398506533 {
|
export class Guid1698398506533 {
|
||||||
// do the migration
|
// do the migration
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { Features } from '../../modules/features';
|
import { Features } from '../../core/features';
|
||||||
import { Quotas } from '../../modules/quota/schema';
|
import { Quotas } from '../../core/quota/schema';
|
||||||
import { migrateNewFeatureTable, upsertFeature } from './utils/user-features';
|
import { migrateNewFeatureTable, upsertFeature } from './utils/user-features';
|
||||||
|
|
||||||
export class UserFeaturesInit1698652531198 {
|
export class UserFeaturesInit1698652531198 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { QuotaType } from '../../modules/quota/types';
|
import { QuotaType } from '../../core/quota/types';
|
||||||
export class OldUserFeature1702620653283 {
|
export class OldUserFeature1702620653283 {
|
||||||
// do the migration
|
// do the migration
|
||||||
static async up(db: PrismaClient) {
|
static async up(db: PrismaClient) {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient, type User } from '@prisma/client';
|
||||||
|
|
||||||
import type { UserType } from '../../modules/users';
|
|
||||||
|
|
||||||
export class UnamedAccount1703756315970 {
|
export class UnamedAccount1703756315970 {
|
||||||
// do the migration
|
// do the migration
|
||||||
@@ -8,7 +6,7 @@ export class UnamedAccount1703756315970 {
|
|||||||
await db.$transaction(async tx => {
|
await db.$transaction(async tx => {
|
||||||
// only find users with empty names
|
// only find users with empty names
|
||||||
const users = await db.$queryRaw<
|
const users = await db.$queryRaw<
|
||||||
UserType[]
|
User[]
|
||||||
>`SELECT * FROM users WHERE name ~ E'^[\\s\\u2000-\\u200F]*$';`;
|
>`SELECT * FROM users WHERE name ~ E'^[\\s\\u2000-\\u200F]*$';`;
|
||||||
console.log(
|
console.log(
|
||||||
`renaming ${users.map(({ email }) => email).join('|')} users`
|
`renaming ${users.map(({ email }) => email).join('|')} users`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { WorkspaceBlobStorage } from '../../modules/storage';
|
import { WorkspaceBlobStorage } from '../../core/storage';
|
||||||
|
|
||||||
export class WorkspaceBlobs1703828796699 {
|
export class WorkspaceBlobs1703828796699 {
|
||||||
// do the migration
|
// do the migration
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { Features } from '../../modules/features';
|
import { Features } from '../../core/features';
|
||||||
import { upsertFeature } from './utils/user-features';
|
import { upsertFeature } from './utils/user-features';
|
||||||
|
|
||||||
export class RefreshUserFeatures1704352562369 {
|
export class RefreshUserFeatures1704352562369 {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
import { FeatureKind } from '../../modules/features';
|
import { FeatureKind } from '../../core/features';
|
||||||
import { Quotas } from '../../modules/quota';
|
import { Quotas } from '../../core/quota';
|
||||||
import { upsertFeature } from './utils/user-features';
|
import { upsertFeature } from './utils/user-features';
|
||||||
|
|
||||||
export class NewFreePlan1705395933447 {
|
export class NewFreePlan1705395933447 {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
CommonFeature,
|
CommonFeature,
|
||||||
FeatureKind,
|
FeatureKind,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
} from '../../../modules/features';
|
} from '../../../core/features';
|
||||||
|
|
||||||
// upgrade features from lower version to higher version
|
// upgrade features from lower version to higher version
|
||||||
export async function upsertFeature(
|
export async function upsertFeature(
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export interface CacheSetOptions {
|
||||||
|
// in milliseconds
|
||||||
|
ttl?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extends if needed
|
||||||
|
export interface Cache {
|
||||||
|
// standard operation
|
||||||
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
||||||
|
set<T = unknown>(
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
opts?: CacheSetOptions
|
||||||
|
): Promise<boolean>;
|
||||||
|
setnx<T = unknown>(
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
opts?: CacheSetOptions
|
||||||
|
): Promise<boolean>;
|
||||||
|
increase(key: string, count?: number): Promise<number>;
|
||||||
|
decrease(key: string, count?: number): Promise<number>;
|
||||||
|
delete(key: string): Promise<boolean>;
|
||||||
|
has(key: string): Promise<boolean>;
|
||||||
|
ttl(key: string): Promise<number>;
|
||||||
|
expire(key: string, ttl: number): Promise<boolean>;
|
||||||
|
|
||||||
|
// list operations
|
||||||
|
pushBack<T = unknown>(key: string, ...values: T[]): Promise<number>;
|
||||||
|
pushFront<T = unknown>(key: string, ...values: T[]): Promise<number>;
|
||||||
|
len(key: string): Promise<number>;
|
||||||
|
list<T = unknown>(key: string, start: number, end: number): Promise<T[]>;
|
||||||
|
popFront<T = unknown>(key: string, count?: number): Promise<T[]>;
|
||||||
|
popBack<T = unknown>(key: string, count?: number): Promise<T[]>;
|
||||||
|
|
||||||
|
// map operations
|
||||||
|
mapSet<T = unknown>(
|
||||||
|
map: string,
|
||||||
|
key: string,
|
||||||
|
value: T,
|
||||||
|
opts: CacheSetOptions
|
||||||
|
): Promise<boolean>;
|
||||||
|
mapIncrease(map: string, key: string, count?: number): Promise<number>;
|
||||||
|
mapDecrease(map: string, key: string, count?: number): Promise<number>;
|
||||||
|
mapGet<T = unknown>(map: string, key: string): Promise<T | undefined>;
|
||||||
|
mapDelete(map: string, key: string): Promise<boolean>;
|
||||||
|
mapKeys(map: string): Promise<string[]>;
|
||||||
|
mapRandomKey(map: string): Promise<string | undefined>;
|
||||||
|
mapLen(map: string): Promise<number>;
|
||||||
|
}
|
||||||
+5
-27
@@ -1,35 +1,13 @@
|
|||||||
import { Global, Module, Provider, Type } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { Redis } from 'ioredis';
|
|
||||||
|
|
||||||
import { SessionCache, ThrottlerCache } from './instances';
|
import { Cache, SessionCache } from './instances';
|
||||||
import { LocalCache } from './providers/cache';
|
|
||||||
import { RedisCache } from './providers/redis';
|
|
||||||
import { CacheRedis, RedisModule, SessionRedis, ThrottlerRedis } from './redis';
|
|
||||||
|
|
||||||
function makeCacheProvider(CacheToken: Type, RedisToken: Type): Provider {
|
|
||||||
return {
|
|
||||||
provide: CacheToken,
|
|
||||||
useFactory: (redis?: Redis) => {
|
|
||||||
return redis ? new RedisCache(redis) : new LocalCache();
|
|
||||||
},
|
|
||||||
inject: [{ token: RedisToken, optional: true }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CacheProvider = makeCacheProvider(LocalCache, CacheRedis);
|
|
||||||
const SessionCacheProvider = makeCacheProvider(SessionCache, SessionRedis);
|
|
||||||
const ThrottlerCacheProvider = makeCacheProvider(
|
|
||||||
ThrottlerCache,
|
|
||||||
ThrottlerRedis
|
|
||||||
);
|
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: AFFiNE.redis.enabled ? [RedisModule] : [],
|
providers: [Cache, SessionCache],
|
||||||
providers: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
|
exports: [Cache, SessionCache],
|
||||||
exports: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
|
|
||||||
})
|
})
|
||||||
export class CacheModule {}
|
export class CacheModule {}
|
||||||
export { LocalCache as Cache, SessionCache, ThrottlerCache };
|
export { Cache, SessionCache };
|
||||||
|
|
||||||
export { CacheInterceptor, MakeCache, PreventCache } from './interceptor';
|
export { CacheInterceptor, MakeCache, PreventCache } from './interceptor';
|
||||||
+12
-3
@@ -1,4 +1,13 @@
|
|||||||
import { LocalCache } from './providers/cache';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
export class SessionCache extends LocalCache {}
|
import { LocalCache } from './local';
|
||||||
export class ThrottlerCache extends LocalCache {}
|
|
||||||
|
@Injectable()
|
||||||
|
export class Cache extends LocalCache {}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionCache extends LocalCache {
|
||||||
|
constructor() {
|
||||||
|
super({ namespace: 'session' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { Reflector } from '@nestjs/core';
|
|||||||
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { mergeMap, Observable, of } from 'rxjs';
|
import { mergeMap, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
import { LocalCache } from './providers/cache';
|
import { Cache } from './instances';
|
||||||
|
|
||||||
export const MakeCache = (key: string[], args?: string[]) =>
|
export const MakeCache = (key: string[], args?: string[]) =>
|
||||||
SetMetadata('cacheKey', [key, args]);
|
SetMetadata('cacheKey', [key, args]);
|
||||||
@@ -24,7 +24,7 @@ export class CacheInterceptor implements NestInterceptor {
|
|||||||
private readonly logger = new Logger(CacheInterceptor.name);
|
private readonly logger = new Logger(CacheInterceptor.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly reflector: Reflector,
|
private readonly reflector: Reflector,
|
||||||
private readonly cache: LocalCache
|
private readonly cache: Cache
|
||||||
) {}
|
) {}
|
||||||
async intercept(
|
async intercept(
|
||||||
ctx: ExecutionContext,
|
ctx: ExecutionContext,
|
||||||
|
|||||||
+1
-51
@@ -1,57 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import Keyv from 'keyv';
|
import Keyv from 'keyv';
|
||||||
|
|
||||||
export interface CacheSetOptions {
|
import type { Cache, CacheSetOptions } from './def';
|
||||||
// in milliseconds
|
|
||||||
ttl?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// extends if needed
|
|
||||||
export interface Cache {
|
|
||||||
// standard operation
|
|
||||||
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
||||||
set<T = unknown>(
|
|
||||||
key: string,
|
|
||||||
value: T,
|
|
||||||
opts?: CacheSetOptions
|
|
||||||
): Promise<boolean>;
|
|
||||||
setnx<T = unknown>(
|
|
||||||
key: string,
|
|
||||||
value: T,
|
|
||||||
opts?: CacheSetOptions
|
|
||||||
): Promise<boolean>;
|
|
||||||
increase(key: string, count?: number): Promise<number>;
|
|
||||||
decrease(key: string, count?: number): Promise<number>;
|
|
||||||
delete(key: string): Promise<boolean>;
|
|
||||||
has(key: string): Promise<boolean>;
|
|
||||||
ttl(key: string): Promise<number>;
|
|
||||||
expire(key: string, ttl: number): Promise<boolean>;
|
|
||||||
|
|
||||||
// list operations
|
|
||||||
pushBack<T = unknown>(key: string, ...values: T[]): Promise<number>;
|
|
||||||
pushFront<T = unknown>(key: string, ...values: T[]): Promise<number>;
|
|
||||||
len(key: string): Promise<number>;
|
|
||||||
list<T = unknown>(key: string, start: number, end: number): Promise<T[]>;
|
|
||||||
popFront<T = unknown>(key: string, count?: number): Promise<T[]>;
|
|
||||||
popBack<T = unknown>(key: string, count?: number): Promise<T[]>;
|
|
||||||
|
|
||||||
// map operations
|
|
||||||
mapSet<T = unknown>(
|
|
||||||
map: string,
|
|
||||||
key: string,
|
|
||||||
value: T,
|
|
||||||
opts: CacheSetOptions
|
|
||||||
): Promise<boolean>;
|
|
||||||
mapIncrease(map: string, key: string, count?: number): Promise<number>;
|
|
||||||
mapDecrease(map: string, key: string, count?: number): Promise<number>;
|
|
||||||
mapGet<T = unknown>(map: string, key: string): Promise<T | undefined>;
|
|
||||||
mapDelete(map: string, key: string): Promise<boolean>;
|
|
||||||
mapKeys(map: string): Promise<string[]>;
|
|
||||||
mapRandomKey(map: string): Promise<string | undefined>;
|
|
||||||
mapLen(map: string): Promise<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LocalCache implements Cache {
|
export class LocalCache implements Cache {
|
||||||
private readonly kv: Keyv;
|
private readonly kv: Keyv;
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Global, Injectable, Module, OnModuleDestroy } from '@nestjs/common';
|
|
||||||
import { Redis as IORedis } from 'ioredis';
|
|
||||||
|
|
||||||
import { Config } from '../../config';
|
|
||||||
|
|
||||||
class Redis extends IORedis implements OnModuleDestroy {
|
|
||||||
onModuleDestroy() {
|
|
||||||
this.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CacheRedis extends Redis {
|
|
||||||
constructor(config: Config) {
|
|
||||||
super({ ...config.redis, db: config.redis.database });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ThrottlerRedis extends Redis {
|
|
||||||
constructor(config: Config) {
|
|
||||||
super({ ...config.redis, db: config.redis.database + 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SessionRedis extends Redis {
|
|
||||||
constructor(config: Config) {
|
|
||||||
super({ ...config.redis, db: config.redis.database + 2 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
providers: [CacheRedis, ThrottlerRedis, SessionRedis],
|
|
||||||
exports: [CacheRedis, ThrottlerRedis, SessionRedis],
|
|
||||||
})
|
|
||||||
export class RedisModule {}
|
|
||||||
@@ -10,13 +10,6 @@ declare global {
|
|||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var AFFiNE: AFFiNEConfig;
|
var AFFiNE: AFFiNEConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
SERVER_FLAVOR: ServerFlavor | '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExternalAccount {
|
export enum ExternalAccount {
|
||||||
@@ -25,22 +18,28 @@ export enum ExternalAccount {
|
|||||||
firebase = 'firebase',
|
firebase = 'firebase',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerFlavor = 'allinone' | 'graphql' | 'sync' | 'selfhosted';
|
export type ServerFlavor =
|
||||||
type ConfigPaths = LeafPaths<
|
| 'allinone'
|
||||||
|
| 'main'
|
||||||
|
// @deprecated
|
||||||
|
| 'graphql'
|
||||||
|
| 'sync'
|
||||||
|
| 'selfhosted';
|
||||||
|
export type ConfigPaths = LeafPaths<
|
||||||
Omit<
|
Omit<
|
||||||
AFFiNEConfig,
|
AFFiNEConfig,
|
||||||
| 'ENV_MAP'
|
| 'ENV_MAP'
|
||||||
| 'version'
|
| 'version'
|
||||||
| 'baseUrl'
|
| 'flavor'
|
||||||
| 'origin'
|
| 'env'
|
||||||
| 'prod'
|
| 'affine'
|
||||||
| 'dev'
|
|
||||||
| 'test'
|
|
||||||
| 'deploy'
|
| 'deploy'
|
||||||
| 'node'
|
| 'node'
|
||||||
|
| 'baseUrl'
|
||||||
|
| 'origin'
|
||||||
>,
|
>,
|
||||||
'',
|
'',
|
||||||
'....'
|
'.....'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,15 +51,28 @@ export interface AFFiNEConfig {
|
|||||||
/**
|
/**
|
||||||
* Server Identity
|
* Server Identity
|
||||||
*/
|
*/
|
||||||
readonly serverId: string;
|
serverId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name may show on the UI
|
||||||
|
*/
|
||||||
|
serverName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* System version
|
* System version
|
||||||
*/
|
*/
|
||||||
readonly version: string;
|
readonly version: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server flavor
|
* Server flavor
|
||||||
*/
|
*/
|
||||||
readonly flavor: ServerFlavor;
|
get flavor(): {
|
||||||
|
type: string;
|
||||||
|
main: boolean;
|
||||||
|
sync: boolean;
|
||||||
|
selfhosted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deployment environment
|
* Deployment environment
|
||||||
*/
|
*/
|
||||||
@@ -172,38 +184,6 @@ export interface AFFiNEConfig {
|
|||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis Config
|
|
||||||
*
|
|
||||||
* whether to use redis as Socket.IO adapter
|
|
||||||
*/
|
|
||||||
redis: {
|
|
||||||
/**
|
|
||||||
* if not enabled, use in-memory adapter by default
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
|
||||||
* url of redis host
|
|
||||||
*/
|
|
||||||
host: string;
|
|
||||||
/**
|
|
||||||
* port of redis
|
|
||||||
*/
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
/**
|
|
||||||
* redis database index
|
|
||||||
*
|
|
||||||
* Rate Limiter scope: database + 1
|
|
||||||
*
|
|
||||||
* Session scope: database + 2
|
|
||||||
*
|
|
||||||
* @default 0
|
|
||||||
*/
|
|
||||||
database: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authentication config
|
* authentication config
|
||||||
*/
|
*/
|
||||||
@@ -341,15 +321,6 @@ export interface AFFiNEConfig {
|
|||||||
metrics: {
|
metrics: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
payment: {
|
|
||||||
stripe: {
|
|
||||||
keys: {
|
|
||||||
APIKey: string;
|
|
||||||
webhookKey: string;
|
|
||||||
};
|
|
||||||
} & import('stripe').Stripe.StripeConfig;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
||||||
|
|
||||||
|
import { merge } from 'lodash-es';
|
||||||
import parse from 'parse-duration';
|
import parse from 'parse-duration';
|
||||||
|
|
||||||
import pkg from '../../../package.json' assert { type: 'json' };
|
import pkg from '../../../package.json' assert { type: 'json' };
|
||||||
@@ -46,11 +47,22 @@ const jwtKeyPair = (function () {
|
|||||||
|
|
||||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||||
let isHttps: boolean | null = null;
|
let isHttps: boolean | null = null;
|
||||||
const flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
|
let flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
serverId: 'affine-nestjs-server',
|
serverId: 'affine-nestjs-server',
|
||||||
|
serverName: flavor === 'selfhosted' ? 'Self-Host Cloud' : 'AFFiNE Cloud',
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
flavor,
|
get flavor() {
|
||||||
|
if (flavor === 'graphql') {
|
||||||
|
flavor = 'main';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: flavor,
|
||||||
|
main: flavor === 'main' || flavor === 'allinone',
|
||||||
|
sync: flavor === 'sync' || flavor === 'allinone',
|
||||||
|
selfhosted: flavor === 'selfhosted',
|
||||||
|
};
|
||||||
|
},
|
||||||
ENV_MAP: {},
|
ENV_MAP: {},
|
||||||
affineEnv: 'dev',
|
affineEnv: 'dev',
|
||||||
get affine() {
|
get affine() {
|
||||||
@@ -142,15 +154,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
|||||||
storage: getDefaultAFFiNEStorageConfig(),
|
storage: getDefaultAFFiNEStorageConfig(),
|
||||||
rateLimiter: {
|
rateLimiter: {
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
limit: 60,
|
limit: 120,
|
||||||
},
|
|
||||||
redis: {
|
|
||||||
enabled: false,
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 6379,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
database: 0,
|
|
||||||
},
|
},
|
||||||
doc: {
|
doc: {
|
||||||
manager: {
|
manager: {
|
||||||
@@ -162,18 +166,16 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
|||||||
interval: 1000 * 60 * 10 /* 10 mins */,
|
interval: 1000 * 60 * 10 /* 10 mins */,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
payment: {
|
|
||||||
stripe: {
|
|
||||||
keys: {
|
|
||||||
APIKey: '',
|
|
||||||
webhookKey: '',
|
|
||||||
},
|
|
||||||
apiVersion: '2023-10-16',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metrics: {
|
metrics: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
enabled: [],
|
||||||
|
use(plugin, config) {
|
||||||
|
this[plugin] = merge(this[plugin], config || {});
|
||||||
|
this.enabled.push(plugin);
|
||||||
|
},
|
||||||
|
},
|
||||||
} satisfies AFFiNEConfig;
|
} satisfies AFFiNEConfig;
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DynamicModule, FactoryProvider } from '@nestjs/common';
|
import { DynamicModule, FactoryProvider } from '@nestjs/common';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
import { ApplyType, DeepPartial } from '../utils/types';
|
import { ApplyType } from '../utils/types';
|
||||||
import { AFFiNEConfig } from './def';
|
import { AFFiNEConfig } from './def';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Snapshot, User, Workspace } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Flatten, Payload } from './types';
|
||||||
|
|
||||||
|
export interface WorkspaceEvents {
|
||||||
|
deleted: Payload<Workspace['id']>;
|
||||||
|
blob: {
|
||||||
|
deleted: Payload<{
|
||||||
|
workspaceId: Workspace['id'];
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocEvents {
|
||||||
|
updated: Payload<
|
||||||
|
Pick<Snapshot, 'id' | 'workspaceId'> & {
|
||||||
|
previous: Pick<Snapshot, 'blob' | 'state' | 'updatedAt'>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserEvents {
|
||||||
|
deleted: Payload<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event definitions can be extended by
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* declare module './event/def' {
|
||||||
|
* interface UserEvents {
|
||||||
|
* created: Payload<User>;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* assert<Event, 'user.created'>()
|
||||||
|
*/
|
||||||
|
export interface EventDefinitions {
|
||||||
|
workspace: WorkspaceEvents;
|
||||||
|
snapshot: DocEvents;
|
||||||
|
user: UserEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventKV = Flatten<EventDefinitions>;
|
||||||
|
|
||||||
|
export type Event = keyof EventKV;
|
||||||
|
export type EventPayload<E extends Event> = EventKV[E];
|
||||||
|
export type { Payload };
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import type { Snapshot, User, Workspace } from '@prisma/client';
|
|
||||||
|
|
||||||
import { Flatten, Payload } from './types';
|
|
||||||
|
|
||||||
interface EventDefinitions {
|
|
||||||
workspace: {
|
|
||||||
deleted: Payload<Workspace['id']>;
|
|
||||||
blob: {
|
|
||||||
deleted: Payload<{
|
|
||||||
workspaceId: Workspace['id'];
|
|
||||||
name: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
snapshot: {
|
|
||||||
updated: Payload<
|
|
||||||
Pick<Snapshot, 'id' | 'workspaceId'> & {
|
|
||||||
previous: Pick<Snapshot, 'blob' | 'state' | 'updatedAt'>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
deleted: Payload<Pick<Snapshot, 'id' | 'workspaceId'>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
user: {
|
|
||||||
deleted: Payload<User>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventKV = Flatten<EventDefinitions>;
|
|
||||||
|
|
||||||
export type Event = keyof EventKV;
|
|
||||||
export type EventPayload<E extends Event> = EventKV[E];
|
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
OnEvent as RawOnEvent,
|
OnEvent as RawOnEvent,
|
||||||
} from '@nestjs/event-emitter';
|
} from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import type { Event, EventPayload } from './events';
|
import type { Event, EventPayload } from './def';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventEmitter {
|
export class EventEmitter {
|
||||||
@@ -40,4 +40,4 @@ export const OnEvent = RawOnEvent as (
|
|||||||
exports: [EventEmitter],
|
exports: [EventEmitter],
|
||||||
})
|
})
|
||||||
export class EventModule {}
|
export class EventModule {}
|
||||||
export { EventPayload };
|
export { Event, EventPayload };
|
||||||
@@ -28,6 +28,7 @@ import { GQLLoggerPlugin } from './logger-plugin';
|
|||||||
? '../../../../node_modules/.cache/schema.gql'
|
? '../../../../node_modules/.cache/schema.gql'
|
||||||
: '../../../schema.gql'
|
: '../../../schema.gql'
|
||||||
),
|
),
|
||||||
|
sortSchema: true,
|
||||||
context: ({ req, res }: { req: Request; res: Response }) => ({
|
context: ({ req, res }: { req: Request; res: Response }) => ({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
export { Cache, CacheInterceptor, MakeCache, PreventCache } from './cache';
|
export {
|
||||||
|
Cache,
|
||||||
|
CacheInterceptor,
|
||||||
|
MakeCache,
|
||||||
|
PreventCache,
|
||||||
|
SessionCache,
|
||||||
|
} from './cache';
|
||||||
export {
|
export {
|
||||||
applyEnvToConfig,
|
applyEnvToConfig,
|
||||||
Config,
|
Config,
|
||||||
|
type ConfigPaths,
|
||||||
getDefaultAFFiNEStorageConfig,
|
getDefaultAFFiNEStorageConfig,
|
||||||
} from './config';
|
} from './config';
|
||||||
export { EventEmitter, type EventPayload, OnEvent } from './event';
|
export { EventEmitter, type EventPayload, OnEvent } from './event';
|
||||||
export { MailService } from './mailer';
|
export { MailService } from './mailer';
|
||||||
export { CallCounter, CallTimer, metrics } from './metrics';
|
export { CallCounter, CallTimer, metrics } from './metrics';
|
||||||
|
export { getOptionalModuleMetadata, OptionalModule } from './nestjs';
|
||||||
export { PrismaService } from './prisma';
|
export { PrismaService } from './prisma';
|
||||||
export { SessionService } from './session';
|
export { SessionService } from './session';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
@@ -17,4 +25,4 @@ export {
|
|||||||
getRequestResponseFromHost,
|
getRequestResponseFromHost,
|
||||||
} from './utils/request';
|
} from './utils/request';
|
||||||
export type * from './utils/types';
|
export type * from './utils/types';
|
||||||
export { RedisIoAdapter } from './websocket';
|
export { SocketIoAdapter } from './websocket';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './optional-module';
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
DynamicModule,
|
||||||
|
Module,
|
||||||
|
ModuleMetadata,
|
||||||
|
Provider,
|
||||||
|
Type,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
|
|
||||||
|
import { Config, ConfigPaths } from '../config';
|
||||||
|
|
||||||
|
interface OptionalModuleMetadata extends ModuleMetadata {
|
||||||
|
/**
|
||||||
|
* Only install module if given config paths are defined in AFFiNE config.
|
||||||
|
*/
|
||||||
|
requires?: ConfigPaths[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only install module if the predication returns true.
|
||||||
|
*/
|
||||||
|
if?: (config: Config) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines which feature will be enabled if the module installed.
|
||||||
|
*/
|
||||||
|
contributesTo?: import('../../core/config').ServerFeature; // avoid circlar dependency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines which providers provided by other modules will be overridden if the module installed.
|
||||||
|
*/
|
||||||
|
overrides?: Provider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalOptions = [
|
||||||
|
'contributesTo',
|
||||||
|
'requires',
|
||||||
|
'if',
|
||||||
|
'overrides',
|
||||||
|
] as const satisfies Array<keyof OptionalModuleMetadata>;
|
||||||
|
|
||||||
|
type OptionalDynamicModule = DynamicModule & OptionalModuleMetadata;
|
||||||
|
|
||||||
|
export function OptionalModule(metadata: OptionalModuleMetadata) {
|
||||||
|
return (target: Type) => {
|
||||||
|
additionalOptions.forEach(option => {
|
||||||
|
if (Object.hasOwn(metadata, option)) {
|
||||||
|
Reflect.defineMetadata(option, metadata[option], target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metadata.overrides) {
|
||||||
|
metadata.providers = (metadata.providers ?? []).concat(
|
||||||
|
metadata.overrides
|
||||||
|
);
|
||||||
|
metadata.exports = (metadata.exports ?? []).concat(metadata.overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestMetadata = omit(metadata, additionalOptions);
|
||||||
|
Module(nestMetadata)(target);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOptionalModuleMetadata<
|
||||||
|
T extends keyof OptionalModuleMetadata,
|
||||||
|
>(target: Type | OptionalDynamicModule, key: T): OptionalModuleMetadata[T] {
|
||||||
|
if ('module' in target) {
|
||||||
|
return target[key];
|
||||||
|
} else {
|
||||||
|
return Reflect.getMetadata(key, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Throttle,
|
Throttle,
|
||||||
@@ -6,40 +6,36 @@ import {
|
|||||||
ThrottlerModule,
|
ThrottlerModule,
|
||||||
ThrottlerModuleOptions,
|
ThrottlerModuleOptions,
|
||||||
ThrottlerOptionsFactory,
|
ThrottlerOptionsFactory,
|
||||||
|
ThrottlerStorageService,
|
||||||
} from '@nestjs/throttler';
|
} from '@nestjs/throttler';
|
||||||
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
|
|
||||||
|
|
||||||
import { ThrottlerCache } from '../cache';
|
|
||||||
import { Config } from '../config';
|
import { Config } from '../config';
|
||||||
import { getRequestResponseFromContext } from '../utils/request';
|
import { getRequestResponseFromContext } from '../utils/request';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ThrottlerStorage extends ThrottlerStorageService {}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class CustomOptionsFactory implements ThrottlerOptionsFactory {
|
class CustomOptionsFactory implements ThrottlerOptionsFactory {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly cache: ThrottlerCache
|
private readonly storage: ThrottlerStorage
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
createThrottlerOptions() {
|
createThrottlerOptions() {
|
||||||
const options: ThrottlerModuleOptions = {
|
const options: ThrottlerModuleOptions = {
|
||||||
throttlers: [
|
throttlers: [
|
||||||
{
|
{
|
||||||
ttl: this.config.rateLimiter.ttl,
|
ttl: this.config.rateLimiter.ttl * 1000,
|
||||||
limit: this.config.rateLimiter.limit,
|
limit: this.config.rateLimiter.limit,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
skipIf: () => {
|
skipIf: () => {
|
||||||
return !this.config.node.prod || this.config.affine.canary;
|
return !this.config.node.prod || this.config.affine.canary;
|
||||||
},
|
},
|
||||||
|
storage: this.storage,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.config.redis.enabled) {
|
|
||||||
new Logger(RateLimiterModule.name).log('Use Redis');
|
|
||||||
options.storage = new ThrottlerStorageRedisService(
|
|
||||||
// @ts-expect-error hidden field
|
|
||||||
this.cache.redis
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,6 +47,8 @@ class CustomOptionsFactory implements ThrottlerOptionsFactory {
|
|||||||
useClass: CustomOptionsFactory,
|
useClass: CustomOptionsFactory,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
providers: [ThrottlerStorage],
|
||||||
|
exports: [ThrottlerStorage],
|
||||||
})
|
})
|
||||||
export class RateLimiterModule {}
|
export class RateLimiterModule {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
export type ConstructorOf<T> = {
|
|
||||||
new (): T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ApplyType<T>(): ConstructorOf<T> {
|
export function ApplyType<T>(): ConstructorOf<T> {
|
||||||
// @ts-expect-error used to fake the type of config
|
// @ts-expect-error used to fake the type of config
|
||||||
return class Inner implements T {
|
return class Inner implements T {
|
||||||
@@ -11,16 +7,6 @@ export function ApplyType<T>(): ConstructorOf<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Array<infer U>
|
|
||||||
? DeepPartial<U>[]
|
|
||||||
: T extends ReadonlyArray<infer U>
|
|
||||||
? ReadonlyArray<DeepPartial<U>>
|
|
||||||
: T extends object
|
|
||||||
? {
|
|
||||||
[K in keyof T]?: DeepPartial<T[K]>;
|
|
||||||
}
|
|
||||||
: T;
|
|
||||||
|
|
||||||
type Join<Prefix, Suffixes> = Prefix extends string | number
|
type Join<Prefix, Suffixes> = Prefix extends string | number
|
||||||
? Suffixes extends string | number
|
? Suffixes extends string | number
|
||||||
? Prefix extends ''
|
? Prefix extends ''
|
||||||
@@ -29,14 +15,6 @@ type Join<Prefix, Suffixes> = Prefix extends string | number
|
|||||||
: never
|
: never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type PrimitiveType =
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| symbol
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
export type LeafPaths<
|
export type LeafPaths<
|
||||||
T,
|
T,
|
||||||
Path extends string = '',
|
Path extends string = '',
|
||||||
|
|||||||
@@ -1 +1,17 @@
|
|||||||
export { RedisIoAdapter } from './redis-adapter';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
|
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||||
|
|
||||||
|
export const SocketIoAdapterImpl = Symbol('SocketIoAdapterImpl');
|
||||||
|
|
||||||
|
export class SocketIoAdapter extends IoAdapter {}
|
||||||
|
|
||||||
|
const SocketIoAdapterImplProvider: Provider = {
|
||||||
|
provide: SocketIoAdapterImpl,
|
||||||
|
useValue: SocketIoAdapter,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [SocketIoAdapterImplProvider],
|
||||||
|
exports: [SocketIoAdapterImplProvider],
|
||||||
|
})
|
||||||
|
export class WebSocketModule {}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
|
||||||
import { createAdapter } from '@socket.io/redis-adapter';
|
|
||||||
import { Redis } from 'ioredis';
|
|
||||||
import { ServerOptions } from 'socket.io';
|
|
||||||
|
|
||||||
export class RedisIoAdapter extends IoAdapter {
|
|
||||||
private adapterConstructor: ReturnType<typeof createAdapter> | undefined;
|
|
||||||
|
|
||||||
async connectToRedis(redis: Redis): Promise<void> {
|
|
||||||
const pubClient = redis;
|
|
||||||
pubClient.on('error', err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
const subClient = pubClient.duplicate();
|
|
||||||
subClient.on('error', err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.adapterConstructor = createAdapter(pubClient, subClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
override createIOServer(port: number, options?: ServerOptions): any {
|
|
||||||
const server = super.createIOServer(port, options);
|
|
||||||
server.adapter(this.adapterConstructor);
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+26
@@ -3,3 +3,29 @@ declare namespace Express {
|
|||||||
user?: import('@prisma/client').User | null;
|
user?: import('@prisma/client').User | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type PrimitiveType =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| symbol
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
declare type ConstructorOf<T> = {
|
||||||
|
new (): T;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type DeepPartial<T> = T extends Array<infer U>
|
||||||
|
? DeepPartial<U>[]
|
||||||
|
: T extends ReadonlyArray<infer U>
|
||||||
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends object
|
||||||
|
? {
|
||||||
|
[K in keyof T]?: DeepPartial<T[K]>;
|
||||||
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
|
declare type AFFiNEModule =
|
||||||
|
| import('@nestjs/common').Type
|
||||||
|
| import('@nestjs/common').DynamicModule;
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { Field, ObjectType, Query } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class ServerConfigType {
|
|
||||||
@Field({ description: 'server version' })
|
|
||||||
version!: string;
|
|
||||||
|
|
||||||
@Field({ description: 'server flavor' })
|
|
||||||
flavor!: string;
|
|
||||||
|
|
||||||
@Field({ description: 'server base url' })
|
|
||||||
baseUrl!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ServerConfigResolver {
|
|
||||||
@Query(() => ServerConfigType, {
|
|
||||||
description: 'server config',
|
|
||||||
})
|
|
||||||
serverConfig(): ServerConfigType {
|
|
||||||
return {
|
|
||||||
version: AFFiNE.version,
|
|
||||||
flavor: AFFiNE.flavor,
|
|
||||||
baseUrl: AFFiNE.baseUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [ServerConfigResolver],
|
|
||||||
})
|
|
||||||
export class ServerConfigModule {}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { join } from 'node:path';
|
|
||||||
|
|
||||||
import { DynamicModule, Type } from '@nestjs/common';
|
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
|
||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
|
||||||
|
|
||||||
import { GqlModule } from '../fundamentals/graphql';
|
|
||||||
import { ServerConfigModule } from './config';
|
|
||||||
import { DocModule } from './doc';
|
|
||||||
import { PaymentModule } from './payment';
|
|
||||||
import { QuotaModule } from './quota';
|
|
||||||
import { SelfHostedModule } from './self-hosted';
|
|
||||||
import { StorageModule } from './storage';
|
|
||||||
import { SyncModule } from './sync';
|
|
||||||
import { UsersModule } from './users';
|
|
||||||
import { WorkspaceModule } from './workspaces';
|
|
||||||
|
|
||||||
const BusinessModules: (Type | DynamicModule)[] = [];
|
|
||||||
|
|
||||||
switch (AFFiNE.flavor) {
|
|
||||||
case 'sync':
|
|
||||||
BusinessModules.push(SyncModule, DocModule);
|
|
||||||
break;
|
|
||||||
case 'selfhosted':
|
|
||||||
BusinessModules.push(
|
|
||||||
ServerConfigModule,
|
|
||||||
SelfHostedModule,
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
GqlModule,
|
|
||||||
WorkspaceModule,
|
|
||||||
UsersModule,
|
|
||||||
SyncModule,
|
|
||||||
DocModule,
|
|
||||||
StorageModule,
|
|
||||||
ServeStaticModule.forRoot({
|
|
||||||
rootPath: join('/app', 'static'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'graphql':
|
|
||||||
BusinessModules.push(
|
|
||||||
ServerConfigModule,
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
GqlModule,
|
|
||||||
WorkspaceModule,
|
|
||||||
UsersModule,
|
|
||||||
DocModule,
|
|
||||||
PaymentModule,
|
|
||||||
QuotaModule,
|
|
||||||
StorageModule
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'allinone':
|
|
||||||
default:
|
|
||||||
BusinessModules.push(
|
|
||||||
ServerConfigModule,
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
GqlModule,
|
|
||||||
WorkspaceModule,
|
|
||||||
UsersModule,
|
|
||||||
QuotaModule,
|
|
||||||
SyncModule,
|
|
||||||
DocModule,
|
|
||||||
PaymentModule,
|
|
||||||
StorageModule
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { BusinessModules };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ResolveField, Resolver } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { UserSubscriptionType } from './payment/resolver';
|
|
||||||
import {
|
|
||||||
SubscriptionPlan,
|
|
||||||
SubscriptionRecurring,
|
|
||||||
SubscriptionStatus,
|
|
||||||
} from './payment/service';
|
|
||||||
import { UserType } from './users';
|
|
||||||
|
|
||||||
const YEAR = 1000 * 60 * 60 * 24 * 30 * 12;
|
|
||||||
|
|
||||||
@Resolver(() => UserType)
|
|
||||||
export class SelfHostedDummyResolver {
|
|
||||||
private readonly start = new Date();
|
|
||||||
private readonly end = new Date(Number(this.start) + YEAR);
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
@ResolveField(() => UserSubscriptionType)
|
|
||||||
async subscription() {
|
|
||||||
return {
|
|
||||||
stripeSubscriptionId: 'dummy',
|
|
||||||
plan: SubscriptionPlan.SelfHosted,
|
|
||||||
recurring: SubscriptionRecurring.Yearly,
|
|
||||||
status: SubscriptionStatus.Active,
|
|
||||||
start: this.start,
|
|
||||||
end: this.end,
|
|
||||||
createdAt: this.start,
|
|
||||||
updatedAt: this.start,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [SelfHostedDummyResolver],
|
|
||||||
})
|
|
||||||
export class SelfHostedModule {}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { PaymentConfig } from './payment';
|
||||||
|
import { RedisOptions } from './redis';
|
||||||
|
|
||||||
|
declare module '../fundamentals/config' {
|
||||||
|
interface PluginsConfig {
|
||||||
|
readonly payment: PaymentConfig;
|
||||||
|
readonly redis: RedisOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AvailablePlugins = keyof PluginsConfig;
|
||||||
|
|
||||||
|
interface AFFiNEConfig {
|
||||||
|
readonly plugins: {
|
||||||
|
enabled: AvailablePlugins[];
|
||||||
|
use<Plugin extends AvailablePlugins>(
|
||||||
|
plugin: Plugin,
|
||||||
|
config?: DeepPartial<PluginsConfig[Plugin]>
|
||||||
|
): void;
|
||||||
|
} & Partial<PluginsConfig>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { AvailablePlugins } from '../fundamentals/config';
|
||||||
|
import { PaymentModule } from './payment';
|
||||||
|
import { RedisModule } from './redis';
|
||||||
|
|
||||||
|
export const pluginsMap = new Map<AvailablePlugins, AFFiNEModule>([
|
||||||
|
['payment', PaymentModule],
|
||||||
|
['redis', RedisModule],
|
||||||
|
]);
|
||||||
+12
-6
@@ -1,15 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { ServerFeature } from '../../core/config';
|
||||||
|
import { FeatureModule } from '../../core/features';
|
||||||
import { FeatureModule } from '../features';
|
import { OptionalModule } from '../../fundamentals';
|
||||||
import { QuotaModule } from '../quota';
|
|
||||||
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
|
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
|
||||||
import { ScheduleManager } from './schedule';
|
import { ScheduleManager } from './schedule';
|
||||||
import { SubscriptionService } from './service';
|
import { SubscriptionService } from './service';
|
||||||
import { StripeProvider } from './stripe';
|
import { StripeProvider } from './stripe';
|
||||||
import { StripeWebhook } from './webhook';
|
import { StripeWebhook } from './webhook';
|
||||||
|
|
||||||
@Module({
|
@OptionalModule({
|
||||||
imports: [FeatureModule, QuotaModule],
|
imports: [FeatureModule],
|
||||||
providers: [
|
providers: [
|
||||||
ScheduleManager,
|
ScheduleManager,
|
||||||
StripeProvider,
|
StripeProvider,
|
||||||
@@ -18,5 +17,12 @@ import { StripeWebhook } from './webhook';
|
|||||||
UserSubscriptionResolver,
|
UserSubscriptionResolver,
|
||||||
],
|
],
|
||||||
controllers: [StripeWebhook],
|
controllers: [StripeWebhook],
|
||||||
|
requires: [
|
||||||
|
'plugins.payment.stripe.keys.APIKey',
|
||||||
|
'plugins.payment.stripe.keys.webhookKey',
|
||||||
|
],
|
||||||
|
contributesTo: ServerFeature.Payment,
|
||||||
})
|
})
|
||||||
export class PaymentModule {}
|
export class PaymentModule {}
|
||||||
|
|
||||||
|
export type { PaymentConfig } from './types';
|
||||||
+27
-6
@@ -16,17 +16,16 @@ import type { User, UserInvoice, UserSubscription } from '@prisma/client';
|
|||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
import { groupBy } from 'lodash-es';
|
import { groupBy } from 'lodash-es';
|
||||||
|
|
||||||
|
import { Auth, CurrentUser, Public } from '../../core/auth';
|
||||||
|
import { UserType } from '../../core/users';
|
||||||
import { Config, PrismaService } from '../../fundamentals';
|
import { Config, PrismaService } from '../../fundamentals';
|
||||||
import { Auth, CurrentUser, Public } from '../auth';
|
import { decodeLookupKey, SubscriptionService } from './service';
|
||||||
import { UserType } from '../users';
|
|
||||||
import {
|
import {
|
||||||
decodeLookupKey,
|
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
SubscriptionRecurring,
|
SubscriptionRecurring,
|
||||||
SubscriptionService,
|
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from './service';
|
} from './types';
|
||||||
|
|
||||||
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
|
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
|
||||||
registerEnumType(SubscriptionRecurring, { name: 'SubscriptionRecurring' });
|
registerEnumType(SubscriptionRecurring, { name: 'SubscriptionRecurring' });
|
||||||
@@ -251,7 +250,10 @@ export class SubscriptionResolver {
|
|||||||
|
|
||||||
@Resolver(() => UserType)
|
@Resolver(() => UserType)
|
||||||
export class UserSubscriptionResolver {
|
export class UserSubscriptionResolver {
|
||||||
constructor(private readonly db: PrismaService) {}
|
constructor(
|
||||||
|
private readonly config: Config,
|
||||||
|
private readonly db: PrismaService
|
||||||
|
) {}
|
||||||
|
|
||||||
@ResolveField(() => UserSubscriptionType, { nullable: true })
|
@ResolveField(() => UserSubscriptionType, { nullable: true })
|
||||||
async subscription(
|
async subscription(
|
||||||
@@ -272,6 +274,25 @@ export class UserSubscriptionResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @FIXME(@forehalo): should not mock any api for selfhosted server
|
||||||
|
// the frontend should avoid calling such api if feature is not enabled
|
||||||
|
if (this.config.flavor.selfhosted) {
|
||||||
|
const start = new Date();
|
||||||
|
const end = new Date();
|
||||||
|
end.setFullYear(start.getFullYear() + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stripeSubscriptionId: 'dummy',
|
||||||
|
plan: SubscriptionPlan.SelfHosted,
|
||||||
|
recurring: SubscriptionRecurring.Yearly,
|
||||||
|
status: SubscriptionStatus.Active,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
createdAt: start,
|
||||||
|
updatedAt: start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return this.db.userSubscription.findUnique({
|
return this.db.userSubscription.findUnique({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
File renamed without changes.
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user