Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support canvas sharing and duplication #547

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy-web-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
VITE_API_URL: https://api.refly.ai
VITE_COLLAB_URL: https://collab.refly.ai
VITE_SUBSCRIPTION_ENABLED: true
VITE_STATIC_PUBLIC_ENDPOINT: https://static.refly.ai

- name: Create Sentry release
uses: getsentry/action-release@v1.7.0
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy-web-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
VITE_API_URL: https://staging-api.refly.ai
VITE_COLLAB_URL: https://staging-collab.refly.ai
VITE_SUBSCRIPTION_ENABLED: true
VITE_STATIC_PUBLIC_ENDPOINT: https://static.refly.ai

- name: Deploy web to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
Expand Down
56 changes: 43 additions & 13 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -248,31 +248,31 @@ model StaticFile {

model Canvas {
/// Primary key
pk BigInt @id @default(autoincrement())
pk BigInt @id @default(autoincrement())
/// Canvas id
canvasId String @unique @map("canvas_id")
canvasId String @unique @map("canvas_id")
/// Owner UID
uid String @map("uid")
uid String @map("uid")
/// Canvas title
title String @default("Untitled") @map("title")
title String @default("Untitled") @map("title")
/// Canvas yjs doc storage size (in bytes)
storageSize BigInt @default(0) @map("storage_size")
storageSize BigInt @default(0) @map("storage_size")
/// Canvas yjs doc storage key
stateStorageKey String? @map("state_storage_key")
stateStorageKey String? @map("state_storage_key")
/// Minimap storage key
minimap String? @map("minimap_storage_key")
minimapStorageKey String? @map("minimap_storage_key")
/// Whether this canvas is readonly
readOnly Boolean @default(false) @map("read_only")
readOnly Boolean @default(false) @map("read_only")
/// Whether this canvas is public
isPublic Boolean @default(false) @map("is_public")
isPublic Boolean @default(false) @map("is_public")
/// Canvas status
status String @default("ready") @map("status")
status String @default("ready") @map("status")
/// Create timestamp
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
/// Update timestamp
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
/// Soft delete timestamp
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()

@@index([uid, updatedAt])
@@map("canvases")
Expand Down Expand Up @@ -458,6 +458,36 @@ model Document {
@@map("documents")
}

model ShareRecord {
/// Primary key
pk BigInt @id @default(autoincrement())
/// Share record id
shareId String @unique @map("share_id")
/// Title
title String @default("") @map("title")
/// Storage key
storageKey String @map("storage_key")
/// UID
uid String @map("uid")
/// Whether to allow duplication of the shared entity
allowDuplication Boolean @default(false) @map("allow_duplication")
/// Parent share id
parentShareId String? @map("parent_share_id")
/// Entity id
entityId String @map("entity_id")
/// Entity type
entityType String @map("entity_type")
/// Create timestamp
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
/// Update timestamp
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz()
/// Soft delete timestamp
deletedAt DateTime? @map("deleted_at") @db.Timestamptz()

@@index([entityType, entityId, deletedAt])
@@map("share_records")
}

model Reference {
/// Primary key
pk BigInt @id @default(autoincrement())
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/action/action.controller.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { GetActionResultResponse } from '@refly-packages/openapi-schema';
import { OptionalJwtAuthGuard } from '@/auth/guard/optional-jwt-auth.guard';
import { LoginedUser } from '@/utils/decorators/user.decorator';
import { User as UserModel } from '@prisma/client';
import { buildSuccessResponse } from '@/utils/response';
import { ActionService } from '@/action/action.service';
import { actionResultPO2DTO } from '@/action/action.dto';
import { JwtAuthGuard } from '@/auth/guard/jwt-auth.guard';

@Controller('v1/action')
export class ActionController {
constructor(private readonly actionService: ActionService) {}

@UseGuards(OptionalJwtAuthGuard)
@UseGuards(JwtAuthGuard)
@Get('/result')
async getActionResult(
@LoginedUser() user: UserModel | null,
@LoginedUser() user: UserModel,
@Query('resultId') resultId: string,
): Promise<GetActionResultResponse> {
const result = await this.actionService.getActionResult(user, { resultId });
Expand Down
18 changes: 2 additions & 16 deletions apps/api/src/action/action.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,21 @@ export class ActionService {
private subscriptionService: SubscriptionService,
) {}

async getActionResult(user: User | null, param: GetActionResultData['query']) {
async getActionResult(user: User, param: GetActionResultData['query']) {
const { resultId, version } = param;

const result = await this.prisma.actionResult.findFirst({
where: {
resultId,
version,
uid: user.uid,
},
orderBy: { version: 'desc' },
});
if (!result) {
throw new ActionResultNotFoundError();
}

if (!user || user.uid !== result.uid) {
const shareRels = await this.prisma.canvasEntityRelation.count({
where: {
entityId: result.resultId,
entityType: 'skillResponse',
isPublic: true,
deletedAt: null,
},
});

if (shareRels === 0) {
throw new ActionResultNotFoundError();
}
}

// If the result is executing and the last updated time is more than 3 minutes ago,
// mark it as failed.
if (result.status === 'executing' && result.updatedAt < new Date(Date.now() - 1000 * 60 * 3)) {
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CanvasModule } from './canvas/canvas.module';
import { CollabModule } from './collab/collab.module';
import { ActionModule } from './action/action.module';
import { RedisService } from '@/common/redis.service';
import { ShareModule } from './share/share.module';

class CustomThrottlerGuard extends ThrottlerGuard {
protected async shouldSkip(context: ExecutionContext): Promise<boolean> {
Expand Down Expand Up @@ -121,6 +122,7 @@ class CustomThrottlerGuard extends ThrottlerGuard {
CanvasModule,
CollabModule,
ActionModule,
ShareModule,
],
controllers: [AppController],
providers: [
Expand Down
57 changes: 0 additions & 57 deletions apps/api/src/auth/guard/optional-jwt-auth.guard.ts

This file was deleted.

7 changes: 3 additions & 4 deletions apps/api/src/canvas/canvas.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
CreateCanvasTemplateRequest,
UpdateCanvasTemplateRequest,
} from '@refly-packages/openapi-schema';
import { OptionalJwtAuthGuard } from '@/auth/guard/optional-jwt-auth.guard';

@Controller('v1/canvas')
export class CanvasController {
Expand All @@ -51,17 +50,17 @@ export class CanvasController {
return buildSuccessResponse(canvasPO2DTO(canvas));
}

@UseGuards(OptionalJwtAuthGuard)
@UseGuards(JwtAuthGuard)
@Get('data')
async getCanvasData(@LoginedUser() user: User | null, @Query('canvasId') canvasId: string) {
async getCanvasData(@LoginedUser() user: User, @Query('canvasId') canvasId: string) {
const data = await this.canvasService.getCanvasRawData(user, canvasId);
return buildSuccessResponse(data);
}

@UseGuards(JwtAuthGuard)
@Post('duplicate')
async duplicateCanvas(@LoginedUser() user: User, @Body() body: DuplicateCanvasRequest) {
const canvas = await this.canvasService.duplicateCanvas(user, body);
const canvas = await this.canvasService.duplicateCanvas(user, body, { checkOwnership: true });
return buildSuccessResponse(canvasPO2DTO(canvas));
}

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/canvas/canvas.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface DuplicateCanvasJobData {

export function canvasPO2DTO(canvas: CanvasModel & { minimapUrl?: string }): Canvas {
return {
...pick(canvas, ['canvasId', 'title', 'isPublic', 'minimapUrl']),
...pick(canvas, ['canvasId', 'title', 'minimapUrl', 'minimapStorageKey']),
createdAt: canvas.createdAt.toJSON(),
updatedAt: canvas.updatedAt.toJSON(),
};
Expand Down
7 changes: 1 addition & 6 deletions apps/api/src/canvas/canvas.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import {
ClearCanvasEntityProcessor,
SyncCanvasEntityProcessor,
AutoNameCanvasProcessor,
DuplicateCanvasProcessor,
} from './canvas.processor';
import { CollabModule } from '@/collab/collab.module';
import { QUEUE_DELETE_KNOWLEDGE_ENTITY, QUEUE_DUPLICATE_CANVAS } from '@/utils/const';
import { QUEUE_DELETE_KNOWLEDGE_ENTITY } from '@/utils/const';
import { CommonModule } from '@/common/common.module';
import { MiscModule } from '@/misc/misc.module';
import { SubscriptionModule } from '@/subscription/subscription.module';
Expand All @@ -27,17 +26,13 @@ import { ActionModule } from '@/action/action.module';
BullModule.registerQueue({
name: QUEUE_DELETE_KNOWLEDGE_ENTITY,
}),
BullModule.registerQueue({
name: QUEUE_DUPLICATE_CANVAS,
}),
],
controllers: [CanvasController],
providers: [
CanvasService,
SyncCanvasEntityProcessor,
ClearCanvasEntityProcessor,
AutoNameCanvasProcessor,
DuplicateCanvasProcessor,
],
exports: [CanvasService],
})
Expand Down
18 changes: 0 additions & 18 deletions apps/api/src/canvas/canvas.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import {
QUEUE_CLEAR_CANVAS_ENTITY,
QUEUE_SYNC_CANVAS_ENTITY,
QUEUE_AUTO_NAME_CANVAS,
QUEUE_DUPLICATE_CANVAS,
} from '@/utils/const';
import {
DeleteCanvasNodesJobData,
SyncCanvasEntityJobData,
AutoNameCanvasJobData,
DuplicateCanvasJobData,
} from './canvas.dto';

@Processor(QUEUE_SYNC_CANVAS_ENTITY)
Expand Down Expand Up @@ -70,19 +68,3 @@ export class AutoNameCanvasProcessor extends WorkerHost {
await this.canvasService.autoNameCanvasFromQueue(job.data);
}
}

@Processor(QUEUE_DUPLICATE_CANVAS)
export class DuplicateCanvasProcessor extends WorkerHost {
private logger = new Logger(DuplicateCanvasProcessor.name);

constructor(private canvasService: CanvasService) {
super();
}

async process(job: Job<DuplicateCanvasJobData>) {
this.logger.log(
`Processing duplicate canvas job ${job.id} for canvas ${job.data.sourceCanvasId}`,
);
await this.canvasService.duplicateCanvasFromQueue(job.data);
}
}
Loading