mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(storage): binding jwst storage to node (#2808)
This commit is contained in:
@@ -1,3 +1,23 @@
|
||||
# Server
|
||||
|
||||
The latest server code of AFFiNE is at https://github.com/toeverything/OctoBase/tree/master/apps/cloud
|
||||
## Get started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build Native binding
|
||||
|
||||
```bash
|
||||
yarn workspace @affine/storage build
|
||||
```
|
||||
|
||||
### Run server
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
now you can access the server GraphQL endpoint at http://localhost:3000/graphql
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "blobs" (
|
||||
"hash" VARCHAR NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"length" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "blobs_pkey" PRIMARY KEY ("hash")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "optimized_blobs" (
|
||||
"hash" VARCHAR NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"params" VARCHAR NOT NULL,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"length" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "optimized_blobs_pkey" PRIMARY KEY ("hash")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "docs" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"workspace_id" VARCHAR NOT NULL,
|
||||
"guid" VARCHAR NOT NULL,
|
||||
"is_workspace" BOOLEAN NOT NULL DEFAULT true,
|
||||
"blob" BYTEA NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "docs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "blobs_workspace_id_hash_key" ON "blobs"("workspace_id", "hash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "optimized_blobs_workspace_id_hash_params_key" ON "optimized_blobs"("workspace_id", "hash", "params");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "docs_workspace_id_guid_idx" ON "docs"("workspace_id", "guid");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "blobs" ADD CONSTRAINT "blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "optimized_blobs" ADD CONSTRAINT "optimized_blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "docs" ADD CONSTRAINT "docs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -15,6 +15,7 @@
|
||||
"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",
|
||||
|
||||
@@ -8,10 +8,13 @@ datasource db {
|
||||
}
|
||||
|
||||
model Workspace {
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
public Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
users UserWorkspacePermission[]
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
public Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
users UserWorkspacePermission[]
|
||||
blobs Blob[]
|
||||
docs Doc[]
|
||||
optimizedBlobs OptimizedBlob[]
|
||||
|
||||
@@map("workspaces")
|
||||
}
|
||||
@@ -86,3 +89,44 @@ model VerificationToken {
|
||||
@@unique([identifier, token])
|
||||
@@map("verificationtokens")
|
||||
}
|
||||
|
||||
model Blob {
|
||||
hash String @id @default(uuid()) @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
length Int
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, hash])
|
||||
@@map("blobs")
|
||||
}
|
||||
|
||||
model OptimizedBlob {
|
||||
hash String @id @default(uuid()) @db.VarChar
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
params String @db.VarChar
|
||||
blob Bytes @db.ByteA
|
||||
length Int
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, hash, params])
|
||||
@@map("optimized_blobs")
|
||||
}
|
||||
|
||||
model Doc {
|
||||
id Int @id @default(autoincrement()) @db.Integer
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
guid String @db.VarChar
|
||||
is_workspace Boolean @default(true) @db.Boolean
|
||||
blob Bytes @db.ByteA
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([workspaceId, guid])
|
||||
@@map("docs")
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ConfigModule } from './config';
|
||||
import { GqlModule } from './graphql.module';
|
||||
import { BusinessModules } from './modules';
|
||||
import { PrismaModule } from './prisma';
|
||||
import { StorageModule } from './storage';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
GqlModule,
|
||||
ConfigModule.forRoot(),
|
||||
StorageModule.forRoot(),
|
||||
...BusinessModules,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -132,6 +132,13 @@ export interface AFFiNEConfig {
|
||||
*/
|
||||
get origin(): string;
|
||||
|
||||
/**
|
||||
* the database config
|
||||
*/
|
||||
db: {
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* the apollo driver config
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
get baseUrl() {
|
||||
return `${this.origin}${this.path}`;
|
||||
},
|
||||
db: {
|
||||
url: '',
|
||||
},
|
||||
graphql: {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
@@ -81,3 +84,5 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { registerEnvs } from './env';
|
||||
|
||||
@@ -2,14 +2,16 @@ import { set } from 'lodash-es';
|
||||
|
||||
import { parseEnvValue } from './def';
|
||||
|
||||
for (const env in AFFiNE.ENV_MAP) {
|
||||
const config = AFFiNE.ENV_MAP[env];
|
||||
const [path, value] =
|
||||
typeof config === 'string'
|
||||
? [config, process.env[env]]
|
||||
: [config[0], parseEnvValue(process.env[env], config[1])];
|
||||
export function registerEnvs() {
|
||||
for (const env in globalThis.AFFiNE.ENV_MAP) {
|
||||
const config = globalThis.AFFiNE.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]);
|
||||
if (typeof value !== 'undefined') {
|
||||
set(globalThis.AFFiNE, path, process.env[env]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// eslint-disable-next-line simple-import-sort/imports
|
||||
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import type { DeepPartial } from '../utils/types';
|
||||
import type { AFFiNEConfig } from './def';
|
||||
|
||||
import '../prelude';
|
||||
|
||||
type ConstructorOf<T> = {
|
||||
new (): T;
|
||||
};
|
||||
@@ -37,11 +40,14 @@ function createConfigProvider(
|
||||
provide: Config,
|
||||
useFactory: () => {
|
||||
const wrapper = new Config();
|
||||
const config = merge({}, AFFiNE, override);
|
||||
const config = merge({}, globalThis.AFFiNE, override);
|
||||
|
||||
const proxy: Config = new Proxy(wrapper, {
|
||||
get: (_target, property: keyof Config) => {
|
||||
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
|
||||
const desc = Object.getOwnPropertyDescriptor(
|
||||
globalThis.AFFiNE,
|
||||
property
|
||||
);
|
||||
if (desc?.get) {
|
||||
return desc.get.call(proxy);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import './prelude';
|
||||
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { static as staticMiddleware } from 'express';
|
||||
|
||||
27
apps/server/src/modules/workspaces/controller.ts
Normal file
27
apps/server/src/modules/workspaces/controller.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@Controller('/api/workspaces')
|
||||
export class WorkspacesController {
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
@Get('/:id/blobs/:name')
|
||||
async blob(
|
||||
@Param('id') workspaceId: string,
|
||||
@Param('name') name: string,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const blob = await this.storage.blob(workspaceId, name);
|
||||
|
||||
if (!blob) {
|
||||
throw new NotFoundException('Blob not found');
|
||||
}
|
||||
|
||||
res.setHeader('content-type', blob.contentType);
|
||||
res.setHeader('last-modified', blob.lastModified);
|
||||
res.setHeader('content-length', blob.size);
|
||||
|
||||
res.send(blob.data);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspacesController } from './controller';
|
||||
import { PermissionService } from './permission';
|
||||
import { WorkspaceResolver } from './resolver';
|
||||
|
||||
@Module({
|
||||
providers: [WorkspaceResolver, PermissionService],
|
||||
providers: [WorkspaceResolver, PermissionService, WorkspacesController],
|
||||
exports: [PermissionService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
@@ -16,8 +17,11 @@ import {
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import type { FileUpload } from '../../types';
|
||||
import { Auth, CurrentUser } from '../auth';
|
||||
import { UserType } from '../users/resolver';
|
||||
import { PermissionService } from './permission';
|
||||
@@ -55,7 +59,8 @@ export class UpdateWorkspaceInput extends PickType(
|
||||
export class WorkspaceResolver {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissionProvider: PermissionService
|
||||
private readonly permissionProvider: PermissionService,
|
||||
private readonly storage: Storage
|
||||
) {}
|
||||
|
||||
@ResolveField(() => Permission, {
|
||||
@@ -174,8 +179,25 @@ export class WorkspaceResolver {
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Create a new workspace',
|
||||
})
|
||||
async createWorkspace(@CurrentUser() user: User) {
|
||||
return this.prisma.workspace.create({
|
||||
async createWorkspace(
|
||||
@CurrentUser() user: User,
|
||||
@Args({ name: 'init', type: () => GraphQLUpload })
|
||||
update: FileUpload
|
||||
) {
|
||||
// convert stream to buffer
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = update.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
|
||||
const workspace = await this.prisma.workspace.create({
|
||||
data: {
|
||||
public: false,
|
||||
users: {
|
||||
@@ -191,6 +213,10 @@ export class WorkspaceResolver {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await this.storage.createWorkspace(workspace.id, buffer);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
@@ -221,8 +247,15 @@ export class WorkspaceResolver {
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.userWorkspacePermission.deleteMany({
|
||||
where: {
|
||||
workspaceId: id,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO:
|
||||
// delete all related data, like websocket connections, blobs, etc.
|
||||
await this.storage.deleteWorkspace(id);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -283,4 +316,28 @@ export class WorkspaceResolver {
|
||||
|
||||
return this.permissionProvider.revoke(workspaceId, user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async uploadBlob(
|
||||
@CurrentUser() user: User,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args({ name: 'blob', type: () => GraphQLUpload })
|
||||
blob: FileUpload
|
||||
) {
|
||||
await this.permissionProvider.check(workspaceId, user.id);
|
||||
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = blob.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
|
||||
return this.storage.uploadBlob(workspaceId, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
|
||||
import { getDefaultAFFiNEConfig } from './config/default';
|
||||
import { getDefaultAFFiNEConfig, registerEnvs } from './config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
globalThis.AFFiNE.ENV_MAP = {
|
||||
DATABASE_URL: 'db.url',
|
||||
};
|
||||
|
||||
registerEnvs();
|
||||
|
||||
@@ -106,7 +106,7 @@ type Mutation {
|
||||
"""
|
||||
Create a new workspace
|
||||
"""
|
||||
createWorkspace: WorkspaceType!
|
||||
createWorkspace(init: Upload!): WorkspaceType!
|
||||
|
||||
"""
|
||||
Update workspace
|
||||
@@ -121,6 +121,7 @@ type Mutation {
|
||||
revoke(workspaceId: String!, userId: String!): Boolean!
|
||||
acceptInvite(workspaceId: String!): Boolean!
|
||||
leaveWorkspace(workspaceId: String!): Boolean!
|
||||
uploadBlob(workspaceId: String!, blob: Upload!): String!
|
||||
|
||||
"""
|
||||
Upload user avatar
|
||||
@@ -128,6 +129,11 @@ type Mutation {
|
||||
uploadAvatar(id: String!, avatar: Upload!): UserType!
|
||||
}
|
||||
|
||||
"""
|
||||
The `Upload` scalar type represents a file upload.
|
||||
"""
|
||||
scalar Upload
|
||||
|
||||
input UpdateWorkspaceInput {
|
||||
"""
|
||||
is Public workspace
|
||||
@@ -135,8 +141,3 @@ input UpdateWorkspaceInput {
|
||||
public: Boolean
|
||||
id: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
The `Upload` scalar type represents a file upload.
|
||||
"""
|
||||
scalar Upload
|
||||
|
||||
23
apps/server/src/storage/index.ts
Normal file
23
apps/server/src/storage/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Storage } from '@affine/storage';
|
||||
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../config';
|
||||
|
||||
export class StorageModule {
|
||||
static forRoot(): DynamicModule {
|
||||
const storageProvider: FactoryProvider = {
|
||||
provide: Storage,
|
||||
useFactory: async (config: Config) => {
|
||||
return Storage.connect(config.db.url);
|
||||
},
|
||||
inject: [Config],
|
||||
};
|
||||
|
||||
return {
|
||||
global: true,
|
||||
module: StorageModule,
|
||||
providers: [storageProvider],
|
||||
exports: [storageProvider],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,9 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
describe('AppModule', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@@ -76,33 +73,14 @@ describe('AppModule', () => {
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
createWorkspace {
|
||||
id
|
||||
public
|
||||
createdAt
|
||||
}
|
||||
query {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
})
|
||||
.expect(200)
|
||||
.expect(res => {
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace === 'object',
|
||||
'res.body.data.createWorkspace is not an object'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.id === 'string',
|
||||
'res.body.data.createWorkspace.id is not a string'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.public === 'boolean',
|
||||
'res.body.data.createWorkspace.public is not a boolean'
|
||||
);
|
||||
ok(
|
||||
typeof res.body.data.createWorkspace.createdAt === 'string',
|
||||
'res.body.data.createWorkspace.created_at is not a string'
|
||||
);
|
||||
ok(res.body.data.__typename === 'Query');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference types="../global.d.ts" />
|
||||
import { ok } from 'node:assert';
|
||||
import { beforeEach, test } from 'node:test';
|
||||
|
||||
@@ -5,14 +6,11 @@ import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
import { ConfigModule } from '../config';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
import { GqlModule } from '../graphql.module';
|
||||
import { AuthModule } from '../modules/auth';
|
||||
import { AuthService } from '../modules/auth/service';
|
||||
import { PrismaModule } from '../prisma';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
let auth: AuthService;
|
||||
|
||||
// cleanup database before each test
|
||||
|
||||
@@ -4,9 +4,6 @@ import { beforeEach, test } from 'node:test';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { Config, ConfigModule } from '../config';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
let config: Config;
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { afterEach, beforeEach, describe, test } from 'node:test';
|
||||
import { afterEach, beforeEach, describe, it } from 'node:test';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
import request from 'supertest';
|
||||
|
||||
import { AppModule } from '../app';
|
||||
import { getDefaultAFFiNEConfig } from '../config/default';
|
||||
import type { TokenType } from '../modules/auth';
|
||||
import type { UserType } from '../modules/users';
|
||||
import type { WorkspaceType } from '../modules/workspaces';
|
||||
|
||||
const gql = '/graphql';
|
||||
|
||||
globalThis.AFFiNE = getDefaultAFFiNEConfig();
|
||||
|
||||
describe('AppModule', () => {
|
||||
describe('Workspace Module', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
// cleanup database before each test
|
||||
@@ -32,6 +31,12 @@ describe('AppModule', () => {
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
app = module.createNestApplication();
|
||||
app.use(
|
||||
graphqlUploadExpress({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxFiles: 5,
|
||||
})
|
||||
);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -63,15 +68,20 @@ describe('AppModule', () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post(gql)
|
||||
.auth(token, { type: 'bearer' })
|
||||
.send({
|
||||
query: `
|
||||
mutation {
|
||||
createWorkspace {
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
name: 'createWorkspace',
|
||||
query: `mutation createWorkspace($init: Upload!) {
|
||||
createWorkspace(init: $init) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
}`,
|
||||
variables: { init: null },
|
||||
})
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.init'] }))
|
||||
.attach('0', Buffer.from([0, 0]), 'init.data')
|
||||
.expect(200);
|
||||
return res.body.data.createWorkspace;
|
||||
}
|
||||
@@ -151,21 +161,21 @@ describe('AppModule', () => {
|
||||
return res.body.data.revoke;
|
||||
}
|
||||
|
||||
test('should register a user', async () => {
|
||||
it('should register a user', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '123456');
|
||||
ok(typeof user.id === 'string', 'user.id is not a string');
|
||||
ok(user.name === 'u1', 'user.name is not valid');
|
||||
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
|
||||
});
|
||||
|
||||
test('should create a workspace', async () => {
|
||||
it('should create a workspace', async () => {
|
||||
const user = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
|
||||
const workspace = await createWorkspace(user.token.token);
|
||||
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
|
||||
});
|
||||
|
||||
test('should invite a user', async () => {
|
||||
it('should invite a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@@ -180,7 +190,7 @@ describe('AppModule', () => {
|
||||
ok(invite === true, 'failed to invite user');
|
||||
});
|
||||
|
||||
test('should accept an invite', async () => {
|
||||
it('should accept an invite', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@@ -191,7 +201,7 @@ describe('AppModule', () => {
|
||||
ok(accept === true, 'failed to accept invite');
|
||||
});
|
||||
|
||||
test('should leave a workspace', async () => {
|
||||
it('should leave a workspace', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
@@ -203,7 +213,7 @@ describe('AppModule', () => {
|
||||
ok(leave === true, 'failed to leave workspace');
|
||||
});
|
||||
|
||||
test('should revoke a user', async () => {
|
||||
it('should revoke a user', async () => {
|
||||
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
|
||||
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/storage/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
||||
Reference in New Issue
Block a user