refactor(server): folder structure (#5573)

This commit is contained in:
liuyi 2024-01-12 04:18:39 +00:00
parent d6f65ea414
commit 89b5c96d25
No known key found for this signature in database
GPG Key ID: 56709255DC7EC728
113 changed files with 453 additions and 482 deletions

View File

@ -1,12 +0,0 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
import { config } from 'dotenv';
import { SERVER_FLAVOR } from './config/default';
if (SERVER_FLAVOR === 'selfhosted') {
config({ path: join(homedir(), '.affine', '.env') });
} else {
config();
}

View File

@ -1,3 +0,0 @@
import { getDefaultAFFiNEConfig } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();

View File

@ -1,13 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import { Config } from './fundamentals/config';
@Controller('/')
export class AppController {
constructor(private readonly config: Config) {}
@Get()
info() {
const version = AFFiNE.version;
return {
compatibility: version,
message: `AFFiNE ${version} Server`,
compatibility: this.config.version,
message: `AFFiNE ${this.config.version} Server`,
};
}
}

View File

@ -2,16 +2,16 @@ import { DynamicModule, Module, Type } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { CacheInterceptor, CacheModule } from './cache';
import { RedisModule } from './cache/redis';
import { ConfigModule, SERVER_FLAVOR } from './config';
import { EventModule } from './event';
import { MetricsModule } from './metrics';
import { CacheInterceptor, CacheModule } from './fundamentals/cache';
import { ConfigModule } from './fundamentals/config';
import { EventModule } from './fundamentals/event';
import { MailModule } from './fundamentals/mailer';
import { MetricsModule } from './fundamentals/metrics';
import { PrismaModule } from './fundamentals/prisma';
import { SessionModule } from './fundamentals/session';
import { RateLimiterModule } from './fundamentals/throttler';
import { BusinessModules } from './modules';
import { AuthModule } from './modules/auth';
import { PrismaModule } from './prisma';
import { SessionModule } from './session';
import { RateLimiterModule } from './throttler';
export const FunctionalityModules: Array<Type | DynamicModule> = [
ConfigModule.forRoot(),
@ -22,13 +22,9 @@ export const FunctionalityModules: Array<Type | DynamicModule> = [
SessionModule,
RateLimiterModule,
AuthModule,
MailModule,
];
// better module registration logic
if (AFFiNE.redis.enabled) {
FunctionalityModules.push(RedisModule);
}
@Module({
providers: [
{
@ -37,6 +33,6 @@ if (AFFiNE.redis.enabled) {
},
],
imports: [...FunctionalityModules, ...BusinessModules],
controllers: SERVER_FLAVOR === 'selfhosted' ? [] : [AppController],
controllers: [AppController],
})
export class AppModule {}

View File

@ -0,0 +1,41 @@
// Convenient way to map environment variables to config values.
AFFiNE.ENV_MAP = {
AFFINE_SERVER_PORT: ['port', 'int'],
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
AFFIHE_SERVER_HTTPS: ['https', 'boolean'],
AFFINE_ENV: 'affineEnv',
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'],
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'],
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_LOGIN: 'auth.email.login',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: ['auth.email.port', 'int'],
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'],
REDIS_SERVER_HOST: 'redis.host',
REDIS_SERVER_PORT: ['redis.port', 'int'],
REDIS_SERVER_USER: 'redis.username',
REDIS_SERVER_PASSWORD: 'redis.password',
REDIS_SERVER_DATABASE: ['redis.database', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithJwstCodec',
'boolean',
],
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
STRIPE_API_KEY: 'payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
};
export default AFFiNE;

View File

@ -1,11 +1,14 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations
const env = process.env;
const node = AFFiNE.node;
// TODO: may be separate config overring in `affine.[env].config`?
// TODO(@forehalo): detail explained
AFFiNE.host = 'localhost';
AFFiNE.port = 3010;
if (node.prod) {
AFFiNE.host = '0.0.0.0';
// Storage
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.storage.providers.r2 = {
@ -29,3 +32,5 @@ if (node.prod) {
// Metrics
AFFiNE.metrics.enabled = true;
}
export default AFFiNE;

View File

@ -1,17 +0,0 @@
import { set } from 'lodash-es';
import { type AFFiNEConfig, parseEnvValue } from './def';
export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
for (const env in rawConfig.ENV_MAP) {
const config = rawConfig.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (value !== undefined) {
set(rawConfig, path, value);
}
}
}

View File

@ -1,3 +0,0 @@
export const OPERATION_NAME = 'x-operation-name';
export const REQUEST_ID = 'x-request-id';

View File

@ -4,7 +4,7 @@ import { Logger, Module } from '@nestjs/common';
import { CommandFactory } from 'nest-commander';
import { AppModule as BusinessAppModule } from '../app';
import { ConfigModule } from '../config';
import { ConfigModule } from '../fundamentals/config';
import { CreateCommand, NameQuestion } from './commands/create';
import { RevertCommand, RunCommand } from './commands/run';
@ -16,9 +16,6 @@ import { RevertCommand, RunCommand } from './commands/run';
enableUpdateAutoMerging: false,
},
},
metrics: {
enabled: false,
},
}),
BusinessAppModule,
],

View File

@ -4,15 +4,14 @@ import { fileURLToPath } from 'node:url';
import { Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { PrismaClient } from '@prisma/client';
import { Command, CommandRunner } from 'nest-commander';
import { PrismaService } from '../../prisma';
interface Migration {
file: string;
name: string;
up: (db: PrismaService, injector: ModuleRef) => Promise<void>;
down: (db: PrismaService, injector: ModuleRef) => Promise<void>;
up: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
down: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
}
export async function collectMigrations(): Promise<Migration[]> {
@ -48,7 +47,7 @@ export async function collectMigrations(): Promise<Migration[]> {
export class RunCommand extends CommandRunner {
logger = new Logger(RunCommand.name);
constructor(
private readonly db: PrismaService,
private readonly db: PrismaClient,
private readonly injector: ModuleRef
) {
super();
@ -139,7 +138,7 @@ export class RevertCommand extends CommandRunner {
logger = new Logger(RevertCommand.name);
constructor(
private readonly db: PrismaService,
private readonly db: PrismaClient,
private readonly injector: ModuleRef
) {
super();

View File

@ -1,7 +1,7 @@
import { PrismaClient } from '@prisma/client';
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
import { DocID } from '../../utils/doc';
import { DocID } from '../../modules/utils/doc';
export class Guid1698398506533 {
// do the migration

View File

@ -4,7 +4,7 @@ import { Redis } from 'ioredis';
import { SessionCache, ThrottlerCache } from './instances';
import { LocalCache } from './providers/cache';
import { RedisCache } from './providers/redis';
import { CacheRedis, SessionRedis, ThrottlerRedis } from './redis';
import { CacheRedis, RedisModule, SessionRedis, ThrottlerRedis } from './redis';
function makeCacheProvider(CacheToken: Type, RedisToken: Type): Provider {
return {
@ -25,6 +25,7 @@ const ThrottlerCacheProvider = makeCacheProvider(
@Global()
@Module({
imports: AFFiNE.redis.enabled ? [RedisModule] : [],
providers: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
exports: [CacheProvider, SessionCacheProvider, ThrottlerCacheProvider],
})

View File

@ -1,6 +1,7 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import type { LeafPaths } from '../utils/types';
import { EnvConfigType } from './env';
import type { AFFiNEStorageConfig } from './storage';
declare global {
@ -18,8 +19,6 @@ export enum ExternalAccount {
}
export type ServerFlavor = 'allinone' | 'graphql' | 'sync' | 'selfhosted';
type EnvConfigType = 'string' | 'int' | 'float' | 'boolean';
type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
@ -31,40 +30,11 @@ type ConfigPaths = LeafPaths<
| 'dev'
| 'test'
| 'deploy'
| 'node'
>,
'',
'....'
>;
/**
* parse number value from environment variables
*/
function int(value: string) {
const n = parseInt(value);
return Number.isNaN(n) ? undefined : n;
}
function float(value: string) {
const n = parseFloat(value);
return Number.isNaN(n) ? undefined : n;
}
function boolean(value: string) {
return value === '1' || value.toLowerCase() === 'true';
}
export function parseEnvValue(value: string | undefined, type?: EnvConfigType) {
if (value === undefined) {
return;
}
return type === 'int'
? int(value)
: type === 'float'
? float(value)
: type === 'boolean'
? boolean(value)
: value;
}
/**
* All Configurations that would control AFFiNE server behaviors
@ -80,6 +50,10 @@ export interface AFFiNEConfig {
* System version
*/
readonly version: string;
/**
* Server flavor
*/
readonly flavor: ServerFlavor;
/**
* Deployment environment
*/
@ -370,3 +344,5 @@ export interface AFFiNEConfig {
} & import('stripe').Stripe.StripeConfig;
};
}
export * from './storage';

View File

@ -1,19 +1,15 @@
/// <reference types="../global.d.ts" />
/// <reference types="../../global.d.ts" />
import { createPrivateKey, createPublicKey } from 'node:crypto';
import parse from 'parse-duration';
import pkg from '../../package.json' assert { type: 'json' };
import pkg from '../../../package.json' assert { type: 'json' };
import type { AFFiNEConfig, ServerFlavor } from './def';
import { applyEnvToConfig } from './env';
import { getDefaultAFFiNEStorageConfig } from './storage';
export const SERVER_FLAVOR = (process.env.SERVER_FLAVOR ??
'allinone') as ServerFlavor;
// Don't use this in production
export const examplePrivateKey = `-----BEGIN EC PRIVATE KEY-----
const examplePrivateKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEtyAJLIULkphVhqXqxk4Nr8Ggty3XLwUJWBxzAWCWTMoAoGCCqGSM49
AwEHoUQDQgAEF3U/0wIeJ3jRKXeFKqQyBKlr9F7xaAUScRrAuSP33rajm3cdfihI
3JvMxVNsS2lE8PSGQrvDrJZaDo0L+Lq9Gg==
@ -50,50 +46,12 @@ const jwtKeyPair = (function () {
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
let isHttps: boolean | null = null;
const flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
const defaultConfig = {
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {
AFFINE_SERVER_PORT: ['port', 'int'],
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
AFFIHE_SERVER_HTTPS: 'https',
AFFINE_ENV: 'affineEnv',
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'],
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'],
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_LOGIN: 'auth.email.login',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: ['auth.email.port', 'int'],
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'],
REDIS_SERVER_HOST: 'redis.host',
REDIS_SERVER_PORT: ['redis.port', 'int'],
REDIS_SERVER_USER: 'redis.username',
REDIS_SERVER_PASSWORD: 'redis.password',
REDIS_SERVER_DATABASE: ['redis.database', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithJwstCodec',
'boolean',
],
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
STRIPE_API_KEY: 'payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: [
'featureFlags.earlyAccessPreview',
'boolean',
],
} satisfies AFFiNEConfig['ENV_MAP'],
flavor,
ENV_MAP: {},
affineEnv: 'dev',
get affine() {
const env = this.affineEnv;
@ -194,7 +152,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
},
doc: {
manager: {
enableUpdateAutoMerging: SERVER_FLAVOR !== 'sync',
enableUpdateAutoMerging: flavor !== 'sync',
updatePollInterval: 3000,
experimentalMergeWithJwstCodec: false,
},
@ -216,7 +174,5 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
},
} satisfies AFFiNEConfig;
applyEnvToConfig(defaultConfig);
return defaultConfig;
};

View File

@ -0,0 +1,50 @@
import { set } from 'lodash-es';
import type { AFFiNEConfig } from './def';
export type EnvConfigType = 'string' | 'int' | 'float' | 'boolean';
/**
* parse number value from environment variables
*/
function int(value: string) {
const n = parseInt(value);
return Number.isNaN(n) ? undefined : n;
}
function float(value: string) {
const n = parseFloat(value);
return Number.isNaN(n) ? undefined : n;
}
function boolean(value: string) {
return value === '1' || value.toLowerCase() === 'true';
}
const envParsers: Record<EnvConfigType, (value: string) => unknown> = {
int,
float,
boolean,
string: value => value,
};
export function parseEnvValue(value: string | undefined, type: EnvConfigType) {
if (value === undefined) {
return;
}
return envParsers[type](value);
}
export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
for (const env in rawConfig.ENV_MAP) {
const config = rawConfig.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, parseEnvValue(process.env[env], 'string')]
: [config[0], parseEnvValue(process.env[env], config[1] ?? 'string')];
if (value !== undefined) {
set(rawConfig, path, value);
}
}
}

View File

@ -0,0 +1,6 @@
// eslint-disable-next-line simple-import-sort/imports
export * from './def';
export * from './default';
export { applyEnvToConfig, parseEnvValue } from './env';
export * from './module';

View File

@ -1,24 +1,12 @@
// eslint-disable-next-line simple-import-sort/imports
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
import { DynamicModule, FactoryProvider } from '@nestjs/common';
import { merge } from 'lodash-es';
import type { DeepPartial } from '../utils/types';
import type { AFFiNEConfig } from './def';
type ConstructorOf<T> = {
new (): T;
};
function ApplyType<T>(): ConstructorOf<T> {
// @ts-expect-error used to fake the type of config
return class Inner implements T {
constructor() {}
};
}
import { ApplyType, DeepPartial } from '../utils/types';
import { AFFiNEConfig } from './def';
/**
* usage:
* ```
* @example
*
* import { Config } from '@affine/server'
*
* class TestConfig {
@ -27,7 +15,6 @@ function ApplyType<T>(): ConstructorOf<T> {
* return this.config.env
* }
* }
* ```
*/
export class Config extends ApplyType<AFFiNEConfig>() {}
@ -69,7 +56,3 @@ export class ConfigModule {
};
};
}
export type { AFFiNEConfig } from './def';
export { SERVER_FLAVOR } from './default';
export * from './storage';

View File

@ -6,8 +6,8 @@ import { Request, Response } from 'express';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { Config } from './config';
import { GQLLoggerPlugin } from './graphql/logger-plugin';
import { Config } from '../config';
import { GQLLoggerPlugin } from './logger-plugin';
@Global()
@Module({
@ -21,12 +21,12 @@ import { GQLLoggerPlugin } from './graphql/logger-plugin';
csrfPrevention: {
requestHeaders: ['content-type'],
},
autoSchemaFile: config.node.test
? join(
fileURLToPath(import.meta.url),
'../../node_modules/.cache/schema.gql'
)
: join(fileURLToPath(import.meta.url), '..', 'schema.gql'),
autoSchemaFile: join(
fileURLToPath(import.meta.url),
config.node.test
? '../../../../node_modules/.cache/schema.gql'
: '../../../schema.gql'
),
context: ({ req, res }: { req: Request; res: Response }) => ({
req,
res,

View File

@ -8,17 +8,22 @@ import { Logger } from '@nestjs/common';
import { Response } from 'express';
import { metrics } from '../metrics/metrics';
import { ReqContext } from '../types';
export interface RequestContext {
req: Express.Request & {
res: Express.Response;
};
}
@Plugin()
export class GQLLoggerPlugin implements ApolloServerPlugin {
protected logger = new Logger(GQLLoggerPlugin.name);
requestDidStart(
reqContext: GraphQLRequestContext<ReqContext>
): Promise<GraphQLRequestListener<GraphQLRequestContext<ReqContext>>> {
const res = reqContext.contextValue.req.res as Response;
const operation = reqContext.request.operationName;
ctx: GraphQLRequestContext<RequestContext>
): Promise<GraphQLRequestListener<GraphQLRequestContext<RequestContext>>> {
const res = ctx.contextValue.req.res as Response;
const operation = ctx.request.operationName;
metrics.gql.counter('query_counter').add(1, { operation });
const start = Date.now();

View File

@ -0,0 +1,20 @@
export { Cache, CacheInterceptor, MakeCache, PreventCache } from './cache';
export {
applyEnvToConfig,
Config,
getDefaultAFFiNEStorageConfig,
} from './config';
export { EventEmitter, type EventPayload, OnEvent } from './event';
export { MailService } from './mailer';
export { CallCounter, CallTimer, metrics } from './metrics';
export { PrismaService } from './prisma';
export { SessionService } from './session';
export * from './storage';
export { AuthThrottlerGuard, CloudThrottlerGuard, Throttle } from './throttler';
export {
getRequestFromHost,
getRequestResponseFromContext,
getRequestResponseFromHost,
} from './utils/request';
export type * from './utils/types';
export { RedisIoAdapter } from './websocket';

View File

@ -0,0 +1,12 @@
import { Global, Module } from '@nestjs/common';
import { MailService } from './mail.service';
import { MAILER } from './mailer';
@Global()
@Module({
providers: [MAILER, MailService],
exports: [MailService],
})
export class MailModule {}
export { MailService };

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Config } from '../../../config';
import { Config } from '../config';
import {
MAILER_SERVICE,
type MailerService,

View File

@ -2,7 +2,7 @@ import { FactoryProvider } from '@nestjs/common';
import { createTransport, Transporter } from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { Config } from '../../../config';
import { Config } from '../config';
export const MAILER_SERVICE = Symbol('MAILER_SERVICE');

View File

@ -1,8 +1,7 @@
import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Config } from '../config';
import { parseEnvValue } from '../config/def';
import { Config, parseEnvValue } from '../config';
import { createSDK, registerCustomMetrics } from './opentelemetry';
@Global()

View File

@ -1,6 +1,6 @@
import { Global, Injectable, Module } from '@nestjs/common';
import { SessionCache } from './cache/instances';
import { SessionCache } from '../cache';
@Injectable()
export class SessionService {

View File

@ -9,10 +9,10 @@ try {
const require = createRequire(import.meta.url);
storageModule =
process.arch === 'arm64'
? require('../../storage.arm64.node')
? require('../.././storage.arm64.node')
: process.arch === 'arm'
? require('../../storage.armv7.node')
: require('../../storage.node');
? require('../../../storage.armv7.node')
: require('../../../storage.node');
}
export { storageModule as OctoBaseStorageModule };
@ -32,3 +32,14 @@ export const mintChallengeResponse = async (resource: string, bits: number) => {
if (!resource) return null;
return storageModule.mintChallengeResponse(resource, bits);
};
export type {
BlobInputType,
BlobOutputType,
GetObjectMetadata,
ListObjectsMetadata,
PutObjectMetadata,
StorageProvider,
} from './providers';
export { createStorageProvider } from './providers';
export { toBuffer } from './providers/utils';

View File

@ -15,7 +15,7 @@ import { join, parse, resolve } from 'node:path';
import { Logger } from '@nestjs/common';
import { Readable } from 'stream';
import { FsStorageConfig } from '../../../config/storage';
import { FsStorageConfig } from '../../config/storage';
import {
BlobInputType,
GetObjectMetadata,

View File

@ -1,4 +1,4 @@
import { AFFiNEStorageConfig, Storages } from '../../../config/storage';
import { AFFiNEStorageConfig, Storages } from '../../config/storage';
import { FsStorageProvider } from './fs';
import type { StorageProvider } from './provider';
import { R2StorageProvider } from './r2';

View File

@ -1,6 +1,6 @@
import type { Readable } from 'node:stream';
import { StorageProviderType } from '../../../config';
import { StorageProviderType } from '../../config';
export interface GetObjectMetadata {
/**

View File

@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common';
import { R2StorageConfig } from '../../../config/storage';
import { R2StorageConfig } from '../../config/storage';
import { S3StorageProvider } from './s3';
export class R2StorageProvider extends S3StorageProvider {

View File

@ -11,7 +11,7 @@ import {
} from '@aws-sdk/client-s3';
import { Logger } from '@nestjs/common';
import { S3StorageConfig } from '../../../config/storage';
import { S3StorageConfig } from '../../config/storage';
import {
BlobInputType,
GetObjectMetadata,

View File

@ -9,9 +9,9 @@ import {
} from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
import { ThrottlerCache } from './cache';
import { Config } from './config';
import { getRequestResponseFromContext } from './utils/nestjs';
import { ThrottlerCache } from '../cache';
import { Config } from '../config';
import { getRequestResponseFromContext } from '../utils/request';
@Injectable()
class CustomOptionsFactory implements ThrottlerOptionsFactory {

View File

@ -1,3 +1,16 @@
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 {
constructor() {}
};
}
export type DeepPartial<T> = T extends Array<infer U>
? DeepPartial<U>[]
: T extends ReadonlyArray<infer U>
@ -40,3 +53,10 @@ export type LeafPaths<
: never;
}[keyof T]
: never;
export interface FileUpload {
filename: string;
mimetype: string;
encoding: string;
createReadStream: () => Readable;
}

View File

@ -0,0 +1 @@
export { RedisIoAdapter } from './redis-adapter';

View File

@ -9,21 +9,16 @@ import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppModule } from './app';
import { Config } from './config';
import { RedisIoAdapter } from './fundamentals';
import { CacheRedis } from './fundamentals/cache/redis';
import { ExceptionLogger } from './middleware/exception-logger';
import { serverTimingAndCache } from './middleware/timing';
import { RedisIoAdapter } from './modules/sync/redis-adapter';
import { CacheRedis } from './cache/redis';
const { NODE_ENV, AFFINE_ENV } = process.env;
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
cors: true,
rawBody: true,
bodyParser: true,
logger:
NODE_ENV !== 'production' || AFFINE_ENV !== 'production'
? ['verbose']
: ['log'],
logger: AFFiNE.affine.stable ? ['log'] : ['verbose'],
});
app.use(serverTimingAndCache);
@ -39,18 +34,16 @@ app.use(
app.useGlobalFilters(new ExceptionLogger());
app.use(cookieParser());
const config = app.get(Config);
const host = config.node.prod ? '0.0.0.0' : 'localhost';
const port = config.port ?? 3010;
if (!config.node.test && config.redis.enabled) {
if (AFFiNE.redis.enabled) {
const redis = app.get(CacheRedis, { strict: false });
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis(redis);
app.useWebSocketAdapter(redisIoAdapter);
}
await app.listen(port, host);
await app.listen(AFFiNE.port, AFFiNE.host);
console.log(`Listening on http://${host}:${port}`);
console.log(
`AFFiNE Server has been started on http://${AFFiNE.host}:${AFFiNE.port}.`
);
console.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`);

View File

@ -9,9 +9,10 @@ import {
import { GqlContextType } from '@nestjs/graphql';
import { Request, Response } from 'express';
import { REQUEST_ID } from '../constants';
const TrivialExceptions = [NotFoundException];
export const REQUEST_ID_HEADER = 'x-request-id';
@Catch()
export class ExceptionLogger implements ExceptionFilter {
private readonly logger = new Logger('ExceptionLogger');
@ -21,7 +22,7 @@ export class ExceptionLogger implements ExceptionFilter {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const requestId = request?.header(REQUEST_ID);
const requestId = request?.header(REQUEST_ID_HEADER);
const shouldVerboseLog = !TrivialExceptions.some(
e => exception instanceof e

View File

@ -10,8 +10,10 @@ import { Reflector } from '@nestjs/core';
import type { NextAuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import { PrismaService } from '../../prisma';
import { getRequestResponseFromContext } from '../../utils/nestjs';
import {
getRequestResponseFromContext,
PrismaService,
} from '../../fundamentals';
import { NextAuthOptionsProvide } from './next-auth-options';
import { AuthService } from './service';

View File

@ -1,7 +1,5 @@
import { Global, Module } from '@nestjs/common';
import { SessionModule } from '../../session';
import { MAILER, MailService } from './mailer';
import { NextAuthController } from './next-auth.controller';
import { NextAuthOptionsProvider } from './next-auth-options';
import { AuthResolver } from './resolver';
@ -9,18 +7,12 @@ import { AuthService } from './service';
@Global()
@Module({
imports: [SessionModule],
providers: [
AuthService,
AuthResolver,
NextAuthOptionsProvider,
MAILER,
MailService,
],
exports: [AuthService, NextAuthOptionsProvider, MailService],
providers: [AuthService, AuthResolver, NextAuthOptionsProvider],
exports: [AuthService, NextAuthOptionsProvider],
controllers: [NextAuthController],
})
export class AuthModule {}
export * from './guard';
export { TokenType } from './resolver';
export { AuthService };

View File

@ -1,2 +0,0 @@
export { MailService } from './mail.service';
export { MAILER } from './mailer';

View File

@ -8,12 +8,14 @@ import Email from 'next-auth/providers/email';
import Github from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { SessionService } from '../../session';
import {
Config,
MailService,
PrismaService,
SessionService,
} from '../../fundamentals';
import { FeatureType } from '../features';
import { Quota_FreePlanV1 } from '../quota';
import { MailService } from './mailer';
import {
decode,
encode,

View File

@ -22,11 +22,14 @@ import { nanoid } from 'nanoid';
import type { AuthAction, CookieOption, NextAuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import { Config } from '../../config';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma/service';
import { SessionService } from '../../session';
import { AuthThrottlerGuard, Throttle } from '../../throttler';
import {
AuthThrottlerGuard,
Config,
metrics,
PrismaService,
SessionService,
Throttle,
} from '../../fundamentals';
import { NextAuthOptionsProvide } from './next-auth-options';
import { AuthService } from './service';

View File

@ -16,9 +16,12 @@ import {
import type { Request } from 'express';
import { nanoid } from 'nanoid';
import { Config } from '../../config';
import { SessionService } from '../../session';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import {
CloudThrottlerGuard,
Config,
SessionService,
Throttle,
} from '../../fundamentals';
import { UserType } from '../users';
import { Auth, CurrentUser } from './guard';
import { AuthService } from './service';

View File

@ -11,11 +11,13 @@ import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import type { User } from '@prisma/client';
import { nanoid } from 'nanoid';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { verifyChallengeResponse } from '../../storage';
import {
Config,
MailService,
PrismaService,
verifyChallengeResponse,
} from '../../fundamentals';
import { Quota_FreePlanV1 } from '../quota';
import { MailService } from './mailer';
export type UserClaim = Pick<
User,
@ -192,6 +194,7 @@ export class AuthService {
name,
email,
password: hashedPassword,
// TODO(@forehalo): handle in event system
features: {
create: {
reason: 'created by api sign up',

View File

@ -4,8 +4,7 @@ import { BadRequestException } from '@nestjs/common';
import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import { JWT } from 'next-auth/jwt';
import { Config } from '../../../config';
import { PrismaService } from '../../../prisma';
import { Config, PrismaService } from '../../../fundamentals';
import { getUtcTimestamp, UserClaim } from '../service';
export const jwtEncode = async (

View File

@ -2,9 +2,7 @@ import { Logger } from '@nestjs/common';
import { nanoid } from 'nanoid';
import type { SendVerificationRequestParams } from 'next-auth/providers/email';
import { Config } from '../../../config';
import { SessionService } from '../../../session';
import { MailService } from '../mailer';
import { Config, MailService, SessionService } from '../../../fundamentals';
export async function sendVerificationRequest(
config: Config,

View File

@ -1,8 +1,6 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query } from '@nestjs/graphql';
import { SERVER_FLAVOR } from '../config';
@ObjectType()
export class ServerConfigType {
@Field({ description: 'server version' })
@ -22,7 +20,7 @@ export class ServerConfigResolver {
serverConfig(): ServerConfigType {
return {
version: AFFiNE.version,
flavor: SERVER_FLAVOR,
flavor: AFFiNE.flavor,
baseUrl: AFFiNE.baseUrl,
};
}

View File

@ -3,10 +3,13 @@ import { isDeepStrictEqual } from 'node:util';
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Config } from '../../config';
import { type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma';
import {
Config,
type EventPayload,
metrics,
OnEvent,
PrismaService,
} from '../../fundamentals';
import { QuotaService } from '../quota';
import { Permission } from '../workspaces/types';
import { isEmptyBuffer } from './manager';

View File

@ -16,12 +16,16 @@ import {
transact,
} from 'yjs';
import { Cache } from '../../cache';
import { Config } from '../../config';
import { EventEmitter, type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics/metrics';
import { PrismaService } from '../../prisma';
import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage';
import {
Cache,
Config,
EventEmitter,
type EventPayload,
mergeUpdatesInApplyWay as jwstMergeUpdates,
metrics,
OnEvent,
PrismaService,
} from '../../fundamentals';
function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
if (yBinary.equals(jwstBinary)) {

View File

@ -1,4 +1,5 @@
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
import { Feature, FeatureSchema, FeatureType } from './types';
class FeatureConfig {
@ -66,7 +67,7 @@ export type FeatureConfigType<F extends FeatureType> = InstanceType<
const FeatureCache = new Map<number, FeatureConfigType<FeatureType>>();
export async function getFeature(prisma: PrismaService, featureId: number) {
export async function getFeature(prisma: PrismaClient, featureId: number) {
const cachedQuota = FeatureCache.get(featureId);
if (cachedQuota) {

View File

@ -1,6 +1,5 @@
import { Module } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { FeatureManagementService } from './management';
import { FeatureService } from './service';
@ -18,4 +17,4 @@ export class FeatureModule {}
export { type CommonFeature, commonFeatureSchema } from './types';
export { FeatureKind, Features, FeatureType } from './types';
export { FeatureManagementService, FeatureService, PrismaService };
export { FeatureManagementService, FeatureService };

View File

@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { Config, PrismaService } from '../../fundamentals';
import { FeatureService } from './service';
import { FeatureType } from './types';

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { UserType } from '../users/types';
import { WorkspaceType } from '../workspaces/types';
import { FeatureConfigType, getFeature } from './feature';

View File

@ -4,8 +4,7 @@ import { DynamicModule, Type } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { SERVER_FLAVOR } from '../config';
import { GqlModule } from '../graphql.module';
import { GqlModule } from '../fundamentals/graphql';
import { ServerConfigModule } from './config';
import { DocModule } from './doc';
import { PaymentModule } from './payment';
@ -18,7 +17,7 @@ import { WorkspaceModule } from './workspaces';
const BusinessModules: (Type | DynamicModule)[] = [];
switch (SERVER_FLAVOR) {
switch (AFFiNE.flavor) {
case 'sync':
BusinessModules.push(SyncModule, DocModule);
break;
@ -68,4 +67,4 @@ switch (SERVER_FLAVOR) {
break;
}
export { BusinessModules, SERVER_FLAVOR };
export { BusinessModules };

View File

@ -16,8 +16,7 @@ import type { User, UserInvoice, UserSubscription } from '@prisma/client';
import { GraphQLError } from 'graphql';
import { groupBy } from 'lodash-es';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { Config, PrismaService } from '../../fundamentals';
import { Auth, CurrentUser, Public } from '../auth';
import { UserType } from '../users';
import {

View File

@ -9,8 +9,7 @@ import type {
} from '@prisma/client';
import Stripe from 'stripe';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { Config, PrismaService } from '../../fundamentals';
import { FeatureManagementService } from '../features';
import { QuotaService, QuotaType } from '../quota';
import { ScheduleManager } from './schedule';

View File

@ -2,7 +2,7 @@ import { FactoryProvider } from '@nestjs/common';
import { omit } from 'lodash-es';
import Stripe from 'stripe';
import { Config } from '../../config';
import { Config } from '../../fundamentals';
export const StripeProvider: FactoryProvider = {
provide: Stripe,

View File

@ -10,7 +10,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import type { Request } from 'express';
import Stripe from 'stripe';
import { Config } from '../../config';
import { Config } from '../../fundamentals';
@Controller('/api/stripe')
export class StripeWebhook {

View File

@ -1,4 +1,4 @@
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { formatDate, formatSize, Quota, QuotaSchema } from './types';
const QuotaCache = new Map<number, QuotaConfig>();

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { FeatureKind } from '../features';
import { QuotaConfig } from './quota';
import { QuotaType } from './types';

View File

@ -1,18 +1,17 @@
import { Injectable } from '@nestjs/common';
import { AFFiNEStorageConfig, Config } from '../../../config';
import { type EventPayload, OnEvent } from '../../../event';
import {
import type {
BlobInputType,
createStorageProvider,
EventPayload,
PutObjectMetadata,
StorageProvider,
} from '../providers';
} from '../../../fundamentals';
import { Config, createStorageProvider, OnEvent } from '../../../fundamentals';
@Injectable()
export class AvatarStorage {
public readonly provider: StorageProvider;
private readonly storageConfig: AFFiNEStorageConfig['storages']['avatar'];
private readonly storageConfig: Config['storage']['storages']['avatar'];
constructor(private readonly config: Config) {
this.provider = createStorageProvider(this.config.storage, 'avatar');

View File

@ -3,15 +3,19 @@ import { Readable } from 'node:stream';
import type { Storage } from '@affine/storage';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Config } from '../../../config';
import { EventEmitter, type EventPayload, OnEvent } from '../../../event';
import { OctoBaseStorageModule } from '../../../storage';
import {
import type {
BlobInputType,
createStorageProvider,
EventPayload,
StorageProvider,
} from '../providers';
import { toBuffer } from '../providers/utils';
} from '../../../fundamentals';
import {
Config,
createStorageProvider,
EventEmitter,
OctoBaseStorageModule,
OnEvent,
toBuffer,
} from '../../../fundamentals';
@Injectable()
export class WorkspaceBlobStorage implements OnModuleInit {

View File

@ -11,12 +11,11 @@ import {
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import { metrics } from '../../../metrics';
import { CallTimer } from '../../../metrics/utils';
import { DocID } from '../../../utils/doc';
import { CallTimer, metrics } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../../workspaces/permission';
import { Permission } from '../../workspaces/types';
import {

View File

@ -1,11 +0,0 @@
export function assertExists<T>(
val: T | null | undefined,
message: string | Error = 'val does not exist'
): asserts val is T {
if (val === null || val === undefined) {
if (message instanceof Error) {
throw message;
}
throw new Error(message);
}
}

View File

@ -5,7 +5,7 @@ import {
} from '@nestjs/common';
import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { CloudThrottlerGuard, Throttle } from '../../fundamentals';
import { Auth, CurrentUser } from '../auth/guard';
import { AuthService } from '../auth/service';
import { FeatureManagementService } from '../features';

View File

@ -11,10 +11,13 @@ import type { User } from '@prisma/client';
import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { EventEmitter } from '../../event';
import { PrismaService } from '../../prisma/service';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import type { FileUpload } from '../../types';
import {
CloudThrottlerGuard,
EventEmitter,
type FileUpload,
PrismaService,
Throttle,
} from '../../fundamentals';
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
import { FeatureManagementService } from '../features';
import { QuotaService } from '../quota';

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
@Injectable()
export class UsersService {

View File

@ -9,13 +9,12 @@ import {
} from '@nestjs/common';
import type { Response } from 'express';
import { CallTimer } from '../../metrics';
import { PrismaService } from '../../prisma';
import { DocID } from '../../utils/doc';
import { CallTimer, PrismaService } from '../../fundamentals';
import { Auth, CurrentUser, Publicable } from '../auth';
import { DocHistoryManager, DocManager } from '../doc';
import { WorkspaceBlobStorage } from '../storage';
import { UserType } from '../users';
import { DocID } from '../utils/doc';
import { PermissionService, PublicPageMode } from './permission';
import { Permission } from './types';

View File

@ -9,7 +9,7 @@ import {
Resolver,
} from '@nestjs/graphql';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { CloudThrottlerGuard, Throttle } from '../../fundamentals';
import { Auth, CurrentUser } from '../auth';
import { FeatureManagementService, FeatureType } from '../features';
import { UserType } from '../users';

View File

@ -1,7 +1,7 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { Permission } from './types';
export enum PublicPageMode {

View File

@ -12,9 +12,12 @@ import {
import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { MakeCache, PreventCache } from '../../../cache';
import { CloudThrottlerGuard } from '../../../throttler';
import type { FileUpload } from '../../../types';
import {
CloudThrottlerGuard,
type FileUpload,
MakeCache,
PreventCache,
} from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService } from '../../quota';

View File

@ -12,11 +12,11 @@ import {
} from '@nestjs/graphql';
import type { SnapshotHistory } from '@prisma/client';
import { CloudThrottlerGuard } from '../../../throttler';
import { DocID } from '../../../utils/doc';
import { CloudThrottlerGuard } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocHistoryManager } from '../../doc/history';
import { DocHistoryManager } from '../../doc';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../permission';
import { Permission, WorkspaceType } from '../types';

View File

@ -11,11 +11,10 @@ import {
} from '@nestjs/graphql';
import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
import { PrismaService } from '../../../prisma';
import { CloudThrottlerGuard } from '../../../throttler';
import { DocID } from '../../../utils/doc';
import { CloudThrottlerGuard, PrismaService } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService, PublicPageMode } from '../permission';
import { Permission, WorkspaceType } from '../types';

View File

@ -20,12 +20,15 @@ import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { applyUpdate, Doc } from 'yjs';
import { EventEmitter } from '../../../event';
import { PrismaService } from '../../../prisma';
import { CloudThrottlerGuard, Throttle } from '../../../throttler';
import type { FileUpload } from '../../../types';
import {
CloudThrottlerGuard,
EventEmitter,
type FileUpload,
MailService,
PrismaService,
Throttle,
} from '../../../fundamentals';
import { Auth, CurrentUser, Public } from '../../auth';
import { MailService } from '../../auth/mailer';
import { AuthService } from '../../auth/service';
import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService } from '../../quota';

View File

@ -1,8 +1,43 @@
import 'reflect-metadata';
import './affine.env';
import './affine';
import './affine.config';
if (process.env.NODE_ENV === 'development') {
console.log('AFFiNE Config:', globalThis.AFFiNE);
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { config } from 'dotenv';
import {
applyEnvToConfig,
getDefaultAFFiNEConfig,
} from './fundamentals/config';
async function load() {
// Initializing AFFiNE config
//
// 1. load dotenv file to `process.env`
// load `.env` under pwd
config();
// load `.env` under user config folder
config({
path: join(fileURLToPath(import.meta.url), '../config/.env'),
});
// 2. generate AFFiNE default config and assign to `globalThis.AFFiNE`
globalThis.AFFiNE = getDefaultAFFiNEConfig();
// TODO(@forehalo):
// Modules may contribute to ENV_MAP, figure out a good way to involve them instead of hardcoding in `./config/affine.env`
// 3. load env => config map to `globalThis.AFFiNE.ENV_MAP
await import('./config/affine.env');
// 4. apply `process.env` map overriding to `globalThis.AFFiNE`
applyEnvToConfig(globalThis.AFFiNE);
// 5. load `./config/affine` to patch custom configs
await import('./config/affine');
if (process.env.NODE_ENV === 'development') {
console.log('AFFiNE Config:', JSON.stringify(globalThis.AFFiNE, null, 2));
}
}
await load();

View File

@ -1,14 +0,0 @@
import { Readable } from 'node:stream';
export interface FileUpload {
filename: string;
mimetype: string;
encoding: string;
createReadStream: () => Readable;
}
export interface ReqContext {
req: Express.Request & {
res: Express.Response;
};
}

View File

@ -4,14 +4,13 @@ import { randomUUID } from 'node:crypto';
import { Transformer } from '@napi-rs/image';
import type { INestApplication } from '@nestjs/common';
import { hashSync } from '@node-rs/argon2';
import { type User } from '@prisma/client';
import { PrismaClient, type User } from '@prisma/client';
import ava, { type TestFn } from 'ava';
import type { Express } from 'express';
import request from 'supertest';
import { AppModule } from '../src/app';
import { FeatureManagementService } from '../src/modules/features';
import { PrismaService } from '../src/prisma/service';
import { createTestingApp } from './utils';
const gql = '/graphql';
@ -52,7 +51,7 @@ test.beforeEach(async t => {
imports: [AppModule],
tapModule(builder) {
builder
.overrideProvider(PrismaService)
.overrideProvider(PrismaClient)
.useClass(FakePrisma)
.overrideProvider(FeatureManagementService)
.useValue({ canEarlyAccess: () => true });

View File

@ -5,7 +5,7 @@ import {
import type { INestApplication } from '@nestjs/common';
import ava, { type TestFn } from 'ava';
import { MailService } from '../src/modules/auth/mailer';
import { MailService } from '../src/fundamentals/mailer';
import { AuthService } from '../src/modules/auth/service';
import {
changeEmail,

View File

@ -2,10 +2,13 @@
import { TestingModule } from '@nestjs/testing';
import test from 'ava';
import { ConfigModule } from '../src/config';
import { ConfigModule } from '../src/fundamentals/config';
import {
mintChallengeResponse,
verifyChallengeResponse,
} from '../src/fundamentals/storage';
import { AuthResolver } from '../src/modules/auth/resolver';
import { AuthService } from '../src/modules/auth/service';
import { mintChallengeResponse, verifyChallengeResponse } from '../src/storage';
import { createTestingModule } from './utils';
let authService: AuthService;

Some files were not shown because too many files have changed in this diff Show More