mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
build: affine Node.js server charts (#2895)
This commit is contained in:
@@ -15,7 +15,6 @@
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/storage": "workspace:*",
|
||||
"@apollo/server": "^4.7.4",
|
||||
"@auth/prisma-adapter": "^1.0.0",
|
||||
"@aws-sdk/client-s3": "^3.359.0",
|
||||
@@ -41,6 +40,7 @@
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/storage": "workspace:*",
|
||||
"@napi-rs/image": "^1.6.1",
|
||||
"@nestjs/testing": "^10.0.3",
|
||||
"@types/express": "^4.17.17",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
13
apps/server/src/app.controller.ts
Normal file
13
apps/server/src/app.controller.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
import pkg from '../package.json' assert { type: 'json' };
|
||||
|
||||
@Controller('/')
|
||||
export class AppController {
|
||||
@Get()
|
||||
hello() {
|
||||
return {
|
||||
message: `AFFiNE GraphQL server: ${pkg.version}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
import { ConfigModule } from './config';
|
||||
import { GqlModule } from './graphql.module';
|
||||
import { BusinessModules } from './modules';
|
||||
@@ -14,5 +15,6 @@ import { StorageModule } from './storage';
|
||||
StorageModule.forRoot(),
|
||||
...BusinessModules,
|
||||
],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface AFFiNEConfig {
|
||||
/**
|
||||
* which port the server will listen on
|
||||
*
|
||||
* @default 3000
|
||||
* @default 3010
|
||||
* @env AFFINE_SERVER_PORT
|
||||
*/
|
||||
port: number;
|
||||
@@ -153,23 +153,13 @@ export interface AFFiNEConfig {
|
||||
/**
|
||||
* whether use remote object storage
|
||||
*/
|
||||
enable: boolean;
|
||||
/**
|
||||
* used to store all uploaded builds and analysis reports
|
||||
*
|
||||
* the concrete type definition is not given here because different storage providers introduce
|
||||
* significant differences in configuration
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* provider: 'aws',
|
||||
* region: 'eu-west-1',
|
||||
* aws_access_key_id: '',
|
||||
* aws_secret_access_key: '',
|
||||
* // other aws storage config...
|
||||
* }
|
||||
*/
|
||||
config: Record<string, string>;
|
||||
r2: {
|
||||
enabled: boolean;
|
||||
accountId: string;
|
||||
bucket: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
};
|
||||
/**
|
||||
* Only used when `enable` is `false`
|
||||
*/
|
||||
@@ -224,6 +214,7 @@ export interface AFFiNEConfig {
|
||||
Record<
|
||||
ExternalAccount,
|
||||
{
|
||||
enabled: boolean;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
|
||||
import { createPrivateKey, createPublicKey } from 'node:crypto';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
@@ -7,82 +8,130 @@ import parse from 'parse-duration';
|
||||
|
||||
import pkg from '../../package.json' assert { type: 'json' };
|
||||
import type { AFFiNEConfig } from './def';
|
||||
import { applyEnvToConfig } from './env';
|
||||
|
||||
// Don't use this in production
|
||||
export const examplePublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnxM+GhB6eNKPmTP6uH5Gpr+bmQ87
|
||||
hHGeOiCsay0w/aPwMqzAOKkZGqX+HZ9BNGy/yiXmnscey5b2vOTzxtRvxA==
|
||||
-----END PUBLIC KEY-----`;
|
||||
export const examplePrivateKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIEtyAJLIULkphVhqXqxk4Nr8Ggty3XLwUJWBxzAWCWTMoAoGCCqGSM49
|
||||
AwEHoUQDQgAEF3U/0wIeJ3jRKXeFKqQyBKlr9F7xaAUScRrAuSP33rajm3cdfihI
|
||||
3JvMxVNsS2lE8PSGQrvDrJZaDo0L+Lq9Gg==
|
||||
-----END EC PRIVATE KEY-----`;
|
||||
|
||||
// Don't use this in production
|
||||
export const examplePrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWOog5SFXs1Vjh/WP
|
||||
QCYPQKgf/jsNmWsvD+jYSn6mi3yhRANCAASfEz4aEHp40o+ZM/q4fkamv5uZDzuE
|
||||
cZ46IKxrLTD9o/AyrMA4qRkapf4dn0E0bL/KJeaexx7Llva85PPG1G/E
|
||||
-----END PRIVATE KEY-----`;
|
||||
const jwtKeyPair = (function () {
|
||||
const AUTH_PRIVATE_KEY = process.env.AUTH_PRIVATE_KEY ?? examplePrivateKey;
|
||||
const privateKey = createPrivateKey({
|
||||
key: Buffer.from(AUTH_PRIVATE_KEY),
|
||||
format: 'pem',
|
||||
type: 'sec1',
|
||||
})
|
||||
.export({
|
||||
format: 'pem',
|
||||
type: 'pkcs8',
|
||||
})
|
||||
.toString('utf8');
|
||||
const publicKey = createPublicKey({
|
||||
key: Buffer.from(AUTH_PRIVATE_KEY),
|
||||
format: 'pem',
|
||||
type: 'spki',
|
||||
})
|
||||
.export({
|
||||
format: 'pem',
|
||||
type: 'spki',
|
||||
})
|
||||
.toString('utf8');
|
||||
|
||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
serverId: 'affine-nestjs-server',
|
||||
version: pkg.version,
|
||||
ENV_MAP: {},
|
||||
env: process.env.NODE_ENV ?? 'development',
|
||||
get prod() {
|
||||
return this.env === 'production';
|
||||
},
|
||||
get dev() {
|
||||
return this.env === 'development';
|
||||
},
|
||||
get test() {
|
||||
return this.env === 'test';
|
||||
},
|
||||
get deploy() {
|
||||
return !this.dev && !this.test;
|
||||
},
|
||||
https: false,
|
||||
host: 'localhost',
|
||||
port: 3010,
|
||||
path: '',
|
||||
get origin() {
|
||||
return this.dev
|
||||
? 'http://localhost:8080'
|
||||
: `${this.https ? 'https' : 'http'}://${this.host}${
|
||||
this.host === 'localhost' ? `:${this.port}` : ''
|
||||
}`;
|
||||
},
|
||||
get baseUrl() {
|
||||
return `${this.origin}${this.path}`;
|
||||
},
|
||||
db: {
|
||||
url: '',
|
||||
},
|
||||
graphql: {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
return {
|
||||
publicKey,
|
||||
privateKey,
|
||||
};
|
||||
})();
|
||||
|
||||
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
const defaultConfig = {
|
||||
serverId: 'affine-nestjs-server',
|
||||
version: pkg.version,
|
||||
ENV_MAP: {
|
||||
AFFINE_SERVER_PORT: 'port',
|
||||
AFFINE_SERVER_HOST: 'host',
|
||||
AFFINE_SERVER_SUB_PATH: 'path',
|
||||
DATABASE_URL: 'db.url',
|
||||
AUTH_PRIVATE_KEY: 'auth.privateKey',
|
||||
ENABLE_R2_OBJECT_STORAGE: 'objectStorage.r2.enabled',
|
||||
R2_OBJECT_STORAGE_ACCOUNT_ID: 'objectStorage.r2.accountId',
|
||||
R2_OBJECT_STORAGE_ACCESS_KEY_ID: 'objectStorage.r2.accessKeyId',
|
||||
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY: 'objectStorage.r2.secretAccessKey',
|
||||
R2_OBJECT_STORAGE_BUCKET: 'objectStorage.r2.bucket',
|
||||
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
|
||||
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
|
||||
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
|
||||
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
|
||||
} satisfies AFFiNEConfig['ENV_MAP'],
|
||||
env: process.env.NODE_ENV ?? 'development',
|
||||
get prod() {
|
||||
return this.env === 'production';
|
||||
},
|
||||
introspection: true,
|
||||
playground: true,
|
||||
debug: true,
|
||||
},
|
||||
auth: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
accessTokenExpiresIn: parse('1h')! / 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
refreshTokenExpiresIn: parse('7d')! / 1000,
|
||||
leeway: 60,
|
||||
publicKey: examplePublicKey,
|
||||
privateKey: examplePrivateKey,
|
||||
enableSignup: true,
|
||||
enableOauth: false,
|
||||
nextAuthSecret: '',
|
||||
oauthProviders: {},
|
||||
},
|
||||
objectStorage: {
|
||||
enable: false,
|
||||
config: {},
|
||||
fs: {
|
||||
path: join(homedir(), '.affine-storage'),
|
||||
get dev() {
|
||||
return this.env === 'development';
|
||||
},
|
||||
},
|
||||
});
|
||||
get test() {
|
||||
return this.env === 'test';
|
||||
},
|
||||
get deploy() {
|
||||
return !this.dev && !this.test;
|
||||
},
|
||||
https: false,
|
||||
host: 'localhost',
|
||||
port: 3010,
|
||||
path: '',
|
||||
db: {
|
||||
url: '',
|
||||
},
|
||||
get origin() {
|
||||
return this.dev
|
||||
? 'http://localhost:8080'
|
||||
: `${this.https ? 'https' : 'http'}://${this.host}${
|
||||
this.host === 'localhost' ? `:${this.port}` : ''
|
||||
}`;
|
||||
},
|
||||
get baseUrl() {
|
||||
return `${this.origin}${this.path}`;
|
||||
},
|
||||
graphql: {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
introspection: true,
|
||||
playground: true,
|
||||
debug: true,
|
||||
},
|
||||
auth: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
accessTokenExpiresIn: parse('1h')! / 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
refreshTokenExpiresIn: parse('7d')! / 1000,
|
||||
leeway: 60,
|
||||
privateKey: jwtKeyPair.privateKey,
|
||||
publicKey: jwtKeyPair.publicKey,
|
||||
enableSignup: true,
|
||||
enableOauth: false,
|
||||
nextAuthSecret: '',
|
||||
oauthProviders: {},
|
||||
},
|
||||
objectStorage: {
|
||||
r2: {
|
||||
enabled: false,
|
||||
bucket: '',
|
||||
accountId: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
},
|
||||
fs: {
|
||||
path: join(homedir(), '.affine-storage'),
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export { registerEnvs } from './env';
|
||||
applyEnvToConfig(defaultConfig);
|
||||
|
||||
return defaultConfig;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { set } from 'lodash-es';
|
||||
|
||||
import { parseEnvValue } from './def';
|
||||
import { type AFFiNEConfig, parseEnvValue } from './def';
|
||||
|
||||
export function registerEnvs() {
|
||||
for (const env in globalThis.AFFiNE.ENV_MAP) {
|
||||
const config = globalThis.AFFiNE.ENV_MAP[env];
|
||||
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 (typeof value !== 'undefined') {
|
||||
set(globalThis.AFFiNE, path, process.env[env]);
|
||||
set(rawConfig, path, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ?? 3010;
|
||||
|
||||
const config = app.get(Config);
|
||||
|
||||
if (!config.objectStorage.enable) {
|
||||
const host = config.host ?? 'localhost';
|
||||
const port = config.port ?? 3010;
|
||||
|
||||
if (!config.objectStorage.r2.enabled) {
|
||||
app.use('/assets', staticMiddleware(config.objectStorage.fs.path));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,14 @@ export const S3_SERVICE = Symbol('S3_SERVICE');
|
||||
export const S3: FactoryProvider<S3Client> = {
|
||||
provide: S3_SERVICE,
|
||||
useFactory: (config: Config) => {
|
||||
const s3 = new S3Client(config.objectStorage.config);
|
||||
const s3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${config.objectStorage.r2.accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: config.objectStorage.r2.accessKeyId,
|
||||
secretAccessKey: config.objectStorage.r2.secretAccessKey,
|
||||
},
|
||||
});
|
||||
return s3;
|
||||
},
|
||||
inject: [Config],
|
||||
|
||||
@@ -15,11 +15,11 @@ export class StorageService {
|
||||
) {}
|
||||
|
||||
async uploadFile(key: string, file: FileUpload) {
|
||||
if (this.config.objectStorage.enable) {
|
||||
if (this.config.objectStorage.r2.enabled) {
|
||||
await this.s3.send(
|
||||
new PutObjectCommand({
|
||||
Body: file.createReadStream(),
|
||||
Bucket: this.config.objectStorage.config.bucket,
|
||||
Bucket: this.config.objectStorage.r2.bucket,
|
||||
Key: key,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
|
||||
import type { Storage } from '@affine/storage';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { StorageProvide } from '../../storage';
|
||||
|
||||
@Controller('/api/workspaces')
|
||||
export class WorkspacesController {
|
||||
constructor(private readonly storage: Storage) {}
|
||||
constructor(@Inject(StorageProvide) private readonly storage: Storage) {}
|
||||
|
||||
@Get('/:id/blobs/:name')
|
||||
async blob(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import type { Storage } from '@affine/storage';
|
||||
import { ForbiddenException, Inject, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Field,
|
||||
@@ -21,6 +21,7 @@ import type { User, Workspace } from '@prisma/client';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { StorageProvide } from '../../storage';
|
||||
import type { FileUpload } from '../../types';
|
||||
import { Auth, CurrentUser } from '../auth';
|
||||
import { UserType } from '../users/resolver';
|
||||
@@ -60,7 +61,7 @@ export class WorkspaceResolver {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissionProvider: PermissionService,
|
||||
private readonly storage: Storage
|
||||
@Inject(StorageProvide) private readonly storage: Storage
|
||||
) {}
|
||||
|
||||
@ResolveField(() => Permission, {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
|
||||
import { getDefaultAFFiNEConfig, registerEnvs } from './config/default';
|
||||
import { getDefaultAFFiNEConfig } from './config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
globalThis.AFFiNE.ENV_MAP = {
|
||||
DATABASE_URL: 'db.url',
|
||||
};
|
||||
|
||||
registerEnvs();
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import type { Storage } from '@affine/storage';
|
||||
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../config';
|
||||
|
||||
export const StorageProvide = Symbol('Storage');
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export class StorageModule {
|
||||
static forRoot(): DynamicModule {
|
||||
const storageProvider: FactoryProvider = {
|
||||
provide: Storage,
|
||||
provide: StorageProvide,
|
||||
useFactory: async (config: Config) => {
|
||||
return Storage.connect(config.db.url);
|
||||
let StorageFactory: typeof Storage;
|
||||
try {
|
||||
// dev mode
|
||||
StorageFactory = (await import('@affine/storage')).Storage;
|
||||
} catch {
|
||||
// In docker
|
||||
StorageFactory = require('../../storage.node').Storage;
|
||||
}
|
||||
return StorageFactory.connect(config.db.url);
|
||||
},
|
||||
inject: [Config],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user