mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 17:39:55 +08:00
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:
@@ -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 { Redis } from 'ioredis';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { SessionCache, ThrottlerCache } 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
|
||||
);
|
||||
import { Cache, SessionCache } from './instances';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: AFFiNE.redis.enabled ? [RedisModule] : [],
|
||||
providers: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
|
||||
exports: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
|
||||
providers: [Cache, SessionCache],
|
||||
exports: [Cache, SessionCache],
|
||||
})
|
||||
export class CacheModule {}
|
||||
export { LocalCache as Cache, SessionCache, ThrottlerCache };
|
||||
export { Cache, SessionCache };
|
||||
|
||||
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 {}
|
||||
export class ThrottlerCache extends LocalCache {}
|
||||
import { LocalCache } from './local';
|
||||
|
||||
@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 { mergeMap, Observable, of } from 'rxjs';
|
||||
|
||||
import { LocalCache } from './providers/cache';
|
||||
import { Cache } from './instances';
|
||||
|
||||
export const MakeCache = (key: string[], args?: string[]) =>
|
||||
SetMetadata('cacheKey', [key, args]);
|
||||
@@ -24,7 +24,7 @@ export class CacheInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(CacheInterceptor.name);
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly cache: LocalCache
|
||||
private readonly cache: Cache
|
||||
) {}
|
||||
async intercept(
|
||||
ctx: ExecutionContext,
|
||||
|
||||
+1
-51
@@ -1,57 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
export interface CacheSetOptions {
|
||||
// in milliseconds
|
||||
ttl?: number;
|
||||
}
|
||||
import type { Cache, CacheSetOptions } from './def';
|
||||
|
||||
// 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 {
|
||||
private readonly kv: Keyv;
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
import { Cache, CacheSetOptions } from './cache';
|
||||
|
||||
export class RedisCache implements Cache {
|
||||
constructor(private readonly redis: Redis) {}
|
||||
|
||||
// standard operation
|
||||
async get<T = unknown>(key: string): Promise<T> {
|
||||
return this.redis
|
||||
.get(key)
|
||||
.then(v => {
|
||||
if (v) {
|
||||
return JSON.parse(v);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async set<T = unknown>(
|
||||
key: string,
|
||||
value: T,
|
||||
opts: CacheSetOptions = {}
|
||||
): Promise<boolean> {
|
||||
if (opts.ttl) {
|
||||
return this.redis
|
||||
.set(key, JSON.stringify(value), 'PX', opts.ttl)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
return this.redis
|
||||
.set(key, JSON.stringify(value))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async increase(key: string, count: number = 1): Promise<number> {
|
||||
return this.redis.incrby(key, count).catch(() => 0);
|
||||
}
|
||||
|
||||
async decrease(key: string, count: number = 1): Promise<number> {
|
||||
return this.redis.decrby(key, count).catch(() => 0);
|
||||
}
|
||||
|
||||
async setnx<T = unknown>(
|
||||
key: string,
|
||||
value: T,
|
||||
opts: CacheSetOptions = {}
|
||||
): Promise<boolean> {
|
||||
if (opts.ttl) {
|
||||
return this.redis
|
||||
.set(key, JSON.stringify(value), 'PX', opts.ttl, 'NX')
|
||||
.then(v => !!v)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
return this.redis
|
||||
.set(key, JSON.stringify(value), 'NX')
|
||||
.then(v => !!v)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
return this.redis
|
||||
.del(key)
|
||||
.then(v => v > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return this.redis
|
||||
.exists(key)
|
||||
.then(v => v > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async ttl(key: string): Promise<number> {
|
||||
return this.redis.ttl(key).catch(() => 0);
|
||||
}
|
||||
|
||||
async expire(key: string, ttl: number): Promise<boolean> {
|
||||
return this.redis
|
||||
.pexpire(key, ttl)
|
||||
.then(v => v > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// list operations
|
||||
async pushBack<T = unknown>(key: string, ...values: T[]): Promise<number> {
|
||||
return this.redis
|
||||
.rpush(key, ...values.map(v => JSON.stringify(v)))
|
||||
.catch(() => 0);
|
||||
}
|
||||
|
||||
async pushFront<T = unknown>(key: string, ...values: T[]): Promise<number> {
|
||||
return this.redis
|
||||
.lpush(key, ...values.map(v => JSON.stringify(v)))
|
||||
.catch(() => 0);
|
||||
}
|
||||
|
||||
async len(key: string): Promise<number> {
|
||||
return this.redis.llen(key).catch(() => 0);
|
||||
}
|
||||
|
||||
async list<T = unknown>(
|
||||
key: string,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<T[]> {
|
||||
return this.redis
|
||||
.lrange(key, start, end)
|
||||
.then(data => data.map(v => JSON.parse(v)))
|
||||
.catch(() => []);
|
||||
}
|
||||
|
||||
async popFront<T = unknown>(key: string, count: number = 1): Promise<T[]> {
|
||||
return this.redis
|
||||
.lpop(key, count)
|
||||
.then(data => (data ?? []).map(v => JSON.parse(v)))
|
||||
.catch(() => []);
|
||||
}
|
||||
|
||||
async popBack<T = unknown>(key: string, count: number = 1): Promise<T[]> {
|
||||
return this.redis
|
||||
.rpop(key, count)
|
||||
.then(data => (data ?? []).map(v => JSON.parse(v)))
|
||||
.catch(() => []);
|
||||
}
|
||||
|
||||
// map operations
|
||||
async mapSet<T = unknown>(
|
||||
map: string,
|
||||
key: string,
|
||||
value: T
|
||||
): Promise<boolean> {
|
||||
return this.redis
|
||||
.hset(map, key, JSON.stringify(value))
|
||||
.then(v => v > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async mapIncrease(
|
||||
map: string,
|
||||
key: string,
|
||||
count: number = 1
|
||||
): Promise<number> {
|
||||
return this.redis.hincrby(map, key, count);
|
||||
}
|
||||
|
||||
async mapDecrease(
|
||||
map: string,
|
||||
key: string,
|
||||
count: number = 1
|
||||
): Promise<number> {
|
||||
return this.redis.hincrby(map, key, -count);
|
||||
}
|
||||
|
||||
async mapGet<T = unknown>(map: string, key: string): Promise<T | undefined> {
|
||||
return this.redis
|
||||
.hget(map, key)
|
||||
.then(v => (v ? JSON.parse(v) : undefined))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async mapDelete(map: string, key: string): Promise<boolean> {
|
||||
return this.redis
|
||||
.hdel(map, key)
|
||||
.then(v => v > 0)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async mapKeys(map: string): Promise<string[]> {
|
||||
return this.redis.hkeys(map).catch(() => []);
|
||||
}
|
||||
|
||||
async mapRandomKey(map: string): Promise<string | undefined> {
|
||||
return this.redis
|
||||
.hrandfield(map, 1)
|
||||
.then(v =>
|
||||
typeof v === 'string'
|
||||
? v
|
||||
: Array.isArray(v)
|
||||
? (v[0] as string)
|
||||
: undefined
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async mapLen(map: string): Promise<number> {
|
||||
return this.redis.hlen(map).catch(() => 0);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
var AFFiNE: AFFiNEConfig;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
SERVER_FLAVOR: ServerFlavor | '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum ExternalAccount {
|
||||
@@ -25,22 +18,28 @@ export enum ExternalAccount {
|
||||
firebase = 'firebase',
|
||||
}
|
||||
|
||||
export type ServerFlavor = 'allinone' | 'graphql' | 'sync' | 'selfhosted';
|
||||
type ConfigPaths = LeafPaths<
|
||||
export type ServerFlavor =
|
||||
| 'allinone'
|
||||
| 'main'
|
||||
// @deprecated
|
||||
| 'graphql'
|
||||
| 'sync'
|
||||
| 'selfhosted';
|
||||
export type ConfigPaths = LeafPaths<
|
||||
Omit<
|
||||
AFFiNEConfig,
|
||||
| 'ENV_MAP'
|
||||
| 'version'
|
||||
| 'baseUrl'
|
||||
| 'origin'
|
||||
| 'prod'
|
||||
| 'dev'
|
||||
| 'test'
|
||||
| 'flavor'
|
||||
| 'env'
|
||||
| 'affine'
|
||||
| 'deploy'
|
||||
| 'node'
|
||||
| 'baseUrl'
|
||||
| 'origin'
|
||||
>,
|
||||
'',
|
||||
'....'
|
||||
'.....'
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -52,15 +51,28 @@ export interface AFFiNEConfig {
|
||||
/**
|
||||
* Server Identity
|
||||
*/
|
||||
readonly serverId: string;
|
||||
serverId: string;
|
||||
|
||||
/**
|
||||
* Name may show on the UI
|
||||
*/
|
||||
serverName: string;
|
||||
|
||||
/**
|
||||
* System version
|
||||
*/
|
||||
readonly version: string;
|
||||
|
||||
/**
|
||||
* Server flavor
|
||||
*/
|
||||
readonly flavor: ServerFlavor;
|
||||
get flavor(): {
|
||||
type: string;
|
||||
main: boolean;
|
||||
sync: boolean;
|
||||
selfhosted: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deployment environment
|
||||
*/
|
||||
@@ -172,38 +184,6 @@ export interface AFFiNEConfig {
|
||||
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
|
||||
*/
|
||||
@@ -341,15 +321,6 @@ export interface AFFiNEConfig {
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
payment: {
|
||||
stripe: {
|
||||
keys: {
|
||||
APIKey: string;
|
||||
webhookKey: string;
|
||||
};
|
||||
} & import('stripe').Stripe.StripeConfig;
|
||||
};
|
||||
}
|
||||
|
||||
export * from './storage';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
||||
|
||||
import { merge } from 'lodash-es';
|
||||
import parse from 'parse-duration';
|
||||
|
||||
import pkg from '../../../package.json' assert { type: 'json' };
|
||||
@@ -46,11 +47,22 @@ const jwtKeyPair = (function () {
|
||||
|
||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
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 = {
|
||||
serverId: 'affine-nestjs-server',
|
||||
serverName: flavor === 'selfhosted' ? 'Self-Host Cloud' : 'AFFiNE Cloud',
|
||||
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: {},
|
||||
affineEnv: 'dev',
|
||||
get affine() {
|
||||
@@ -142,15 +154,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
storage: getDefaultAFFiNEStorageConfig(),
|
||||
rateLimiter: {
|
||||
ttl: 60,
|
||||
limit: 60,
|
||||
},
|
||||
redis: {
|
||||
enabled: false,
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
username: '',
|
||||
password: '',
|
||||
database: 0,
|
||||
limit: 120,
|
||||
},
|
||||
doc: {
|
||||
manager: {
|
||||
@@ -162,18 +166,16 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
interval: 1000 * 60 * 10 /* 10 mins */,
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
stripe: {
|
||||
keys: {
|
||||
APIKey: '',
|
||||
webhookKey: '',
|
||||
},
|
||||
apiVersion: '2023-10-16',
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
enabled: false,
|
||||
},
|
||||
plugins: {
|
||||
enabled: [],
|
||||
use(plugin, config) {
|
||||
this[plugin] = merge(this[plugin], config || {});
|
||||
this.enabled.push(plugin);
|
||||
},
|
||||
},
|
||||
} satisfies AFFiNEConfig;
|
||||
|
||||
return defaultConfig;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicModule, FactoryProvider } from '@nestjs/common';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import { ApplyType, DeepPartial } from '../utils/types';
|
||||
import { ApplyType } from '../utils/types';
|
||||
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,
|
||||
} from '@nestjs/event-emitter';
|
||||
|
||||
import type { Event, EventPayload } from './events';
|
||||
import type { Event, EventPayload } from './def';
|
||||
|
||||
@Injectable()
|
||||
export class EventEmitter {
|
||||
@@ -40,4 +40,4 @@ export const OnEvent = RawOnEvent as (
|
||||
exports: [EventEmitter],
|
||||
})
|
||||
export class EventModule {}
|
||||
export { EventPayload };
|
||||
export { Event, EventPayload };
|
||||
|
||||
@@ -28,6 +28,7 @@ import { GQLLoggerPlugin } from './logger-plugin';
|
||||
? '../../../../node_modules/.cache/schema.gql'
|
||||
: '../../../schema.gql'
|
||||
),
|
||||
sortSchema: true,
|
||||
context: ({ req, res }: { req: Request; res: Response }) => ({
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
export { Cache, CacheInterceptor, MakeCache, PreventCache } from './cache';
|
||||
export {
|
||||
Cache,
|
||||
CacheInterceptor,
|
||||
MakeCache,
|
||||
PreventCache,
|
||||
SessionCache,
|
||||
} from './cache';
|
||||
export {
|
||||
applyEnvToConfig,
|
||||
Config,
|
||||
type ConfigPaths,
|
||||
getDefaultAFFiNEStorageConfig,
|
||||
} from './config';
|
||||
export { EventEmitter, type EventPayload, OnEvent } from './event';
|
||||
export { MailService } from './mailer';
|
||||
export { CallCounter, CallTimer, metrics } from './metrics';
|
||||
export { getOptionalModuleMetadata, OptionalModule } from './nestjs';
|
||||
export { PrismaService } from './prisma';
|
||||
export { SessionService } from './session';
|
||||
export * from './storage';
|
||||
@@ -17,4 +25,4 @@ export {
|
||||
getRequestResponseFromHost,
|
||||
} from './utils/request';
|
||||
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 {
|
||||
Throttle,
|
||||
@@ -6,40 +6,36 @@ import {
|
||||
ThrottlerModule,
|
||||
ThrottlerModuleOptions,
|
||||
ThrottlerOptionsFactory,
|
||||
ThrottlerStorageService,
|
||||
} from '@nestjs/throttler';
|
||||
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
|
||||
|
||||
import { ThrottlerCache } from '../cache';
|
||||
import { Config } from '../config';
|
||||
import { getRequestResponseFromContext } from '../utils/request';
|
||||
|
||||
@Injectable()
|
||||
export class ThrottlerStorage extends ThrottlerStorageService {}
|
||||
|
||||
@Injectable()
|
||||
class CustomOptionsFactory implements ThrottlerOptionsFactory {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly cache: ThrottlerCache
|
||||
private readonly storage: ThrottlerStorage
|
||||
) {}
|
||||
|
||||
createThrottlerOptions() {
|
||||
const options: ThrottlerModuleOptions = {
|
||||
throttlers: [
|
||||
{
|
||||
ttl: this.config.rateLimiter.ttl,
|
||||
ttl: this.config.rateLimiter.ttl * 1000,
|
||||
limit: this.config.rateLimiter.limit,
|
||||
},
|
||||
],
|
||||
skipIf: () => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +47,8 @@ class CustomOptionsFactory implements ThrottlerOptionsFactory {
|
||||
useClass: CustomOptionsFactory,
|
||||
}),
|
||||
],
|
||||
providers: [ThrottlerStorage],
|
||||
exports: [ThrottlerStorage],
|
||||
})
|
||||
export class RateLimiterModule {}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
export type ConstructorOf<T> = {
|
||||
new (): T;
|
||||
};
|
||||
|
||||
export function ApplyType<T>(): ConstructorOf<T> {
|
||||
// @ts-expect-error used to fake the type of config
|
||||
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
|
||||
? Suffixes extends string | number
|
||||
? Prefix extends ''
|
||||
@@ -29,14 +15,6 @@ type Join<Prefix, Suffixes> = Prefix extends string | number
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type PrimitiveType =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| symbol
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type LeafPaths<
|
||||
T,
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user