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: add comment feature #19

Merged
merged 17 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions packages/server/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,54 @@ type Organization {
logoURL: String!
}

type UserModel {
id: ID!
}

type Comment {
_id: ID!
file: File!
parentId: ID
user: UserModel!
date: DateTime!
content: String!
replies: [Comment!]!
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type File {
_id: ID!
fileId: String!
bucket: String!
comments: [Comment!]!
}

type Query {
getOriganizations: [Organization!]!
getFileByFileId(fileId: String!): File!
}

type Mutation {
addFile(input: CreateFileInput!): File!
deleteFile(fileId: String!): Boolean!
addComment(input: CreateCommentInput!): Comment!
deleteComment(id: String!): Boolean!

"""Create new JupyterNotebook for user"""
nistGetJupterNotebook(fileURL: String!, fileName: String!): String!
}

input CreateFileInput {
fileId: String!
bucket: String!
}

input CreateCommentInput {
file: String!
parentId: String
content: String!
}
7 changes: 5 additions & 2 deletions packages/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JupyterhubModule } from './jupyterhub/jupyterhub.module';
import configuration from './config/configuration';
import { FileModule } from './file/file.module';
import { CommentModule } from './comment/comment.module';

@Module({
imports: [
Expand All @@ -30,8 +32,9 @@ import configuration from './config/configuration';
inject: [ConfigService]
}),
OrganizationModule,
JupyterhubModule,
AuthModule
wenhwang97 marked this conversation as resolved.
Show resolved Hide resolved
FileModule,
CommentModule,
JupyterhubModule
]
})
export class AppModule {}
13 changes: 13 additions & 0 deletions packages/server/src/comment/comment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class CreateCommentInput {
@Field()
file: string;

@Field({ nullable: true })
parentId?: string;

@Field()
content: string;
}
49 changes: 49 additions & 0 deletions packages/server/src/comment/comment.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { File } from 'src/file/file.model';

@ObjectType()
@Directive('@key(fields: "id")')
@Directive('@extends')
export class UserModel {
@Field(() => ID)
@Directive('@external')
id: string;
}

@Schema()
@ObjectType()
@Directive('@key(fields: "_id")')
export class Comment {
@Field(() => ID)
_id: mongoose.Schema.Types.ObjectId;

// refer to fileId in file.model.ts, NOT _id
@Prop({ required: true })
@Field(() => File, { description: "UUID of the file which is fileId in file's model" })
file: string;

@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Comment', required: false })
@Field(() => ID, { nullable: true })
parentId: mongoose.Schema.Types.ObjectId | null;

@Prop()
@Field(() => UserModel, { description: 'ID of the user from the Auth Microservice' })
user: string;

@Prop({ type: Date, required: true, default: Date.now })
@Field()
date: Date;

@Prop()
@Field()
content: string;

@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }], default: [] })
@Field(() => [Comment])
replies: mongoose.Schema.Types.ObjectId[];
}

export type CommentDocument = Comment & Document;
export const CommentSchema = SchemaFactory.createForClass(Comment);
18 changes: 18 additions & 0 deletions packages/server/src/comment/comment.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Comment, CommentSchema } from './comment.model';
import { CommentService } from './comment.service';
import { CommentResolver } from './comment.resolver';
import { File, FileSchema } from 'src/file/file.model';
import { FileService } from 'src/file/file.service';

@Module({
imports: [
MongooseModule.forFeature([
{ name: Comment.name, schema: CommentSchema },
{ name: File.name, schema: FileSchema }
])
],
providers: [CommentService, CommentResolver, FileService]
})
export class CommentModule {}
41 changes: 41 additions & 0 deletions packages/server/src/comment/comment.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Args, Mutation, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { CommentService } from './comment.service';
import { Comment, UserModel } from './comment.model';
import { CreateCommentInput } from './comment.dto';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt.guard';
import { UserContext } from 'src/auth/user.decorator';
import { TokenPayload } from 'src/auth/user.dto';
import { FileService } from 'src/file/file.service';
import { File } from 'src/file/file.model';

@Resolver(() => Comment)
@UseGuards(JwtAuthGuard)
export class CommentResolver {
constructor(private commentService: CommentService, private fileService: FileService) {}

@Mutation(() => Comment)
async addComment(@UserContext() user: TokenPayload, @Args('input') input: CreateCommentInput): Promise<Comment> {
return this.commentService.create(input, user.id);
}

@Mutation(() => Boolean)
async deleteComment(@Args('id') id: string): Promise<boolean> {
return this.commentService.removeComment(id);
}
wenhwang97 marked this conversation as resolved.
Show resolved Hide resolved

@ResolveField('replies', () => [Comment])
async getReplies(@Parent() comment: Comment): Promise<Comment[]> {
return this.commentService.findByIds(comment.replies);
}

@ResolveField('file', () => File, { nullable: true })
async getFile(@Parent() comment: Comment): Promise<File | null> {
return this.fileService.findByFileId(comment.file);
}

@ResolveField('user', () => UserModel)
resolveUser(@Parent() comment: Comment): any {
return { __typename: 'UserModel', id: comment.user };
}
}
41 changes: 41 additions & 0 deletions packages/server/src/comment/comment.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Comment, CommentDocument } from './comment.model';
import mongoose, { Model } from 'mongoose';
import { CreateCommentInput } from './comment.dto';
import { File, FileDocument } from 'src/file/file.model';

@Injectable()
export class CommentService {
constructor(
@InjectModel(Comment.name) private commentModel: Model<CommentDocument>,
@InjectModel(File.name) private fileModel: Model<FileDocument>
) {}

async create(createCommentInput: CreateCommentInput, user: string): Promise<Comment> {
const newComment = new this.commentModel({ ...createCommentInput, user });
await newComment.save();

if (createCommentInput.parentId) {
await this.commentModel.updateOne({ _id: createCommentInput.parentId }, { $push: { replies: newComment._id } });
} else {
await this.fileModel.updateOne({ file: createCommentInput.file }, { $push: { comments: newComment._id } });
}

return newComment;
}

async findByIds(ids: mongoose.Schema.Types.ObjectId[]): Promise<Comment[]> {
return this.commentModel.find({ _id: { $in: ids } }).exec();
}

async findByFile(file: string): Promise<Comment[]> {
return this.commentModel.find({ file, parentId: null }).exec();
}

async removeComment(id: string): Promise<boolean> {
await this.commentModel.deleteMany({ parentId: id }).exec();
const deleted = await this.commentModel.findByIdAndDelete(id).exec();
return !!deleted;
}
}
10 changes: 10 additions & 0 deletions packages/server/src/file/file.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Field, ID, InputType } from '@nestjs/graphql';

@InputType()
export class CreateFileInput {
@Field()
fileId: string;

@Field()
bucket: string;
}
29 changes: 29 additions & 0 deletions packages/server/src/file/file.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { Comment } from 'src/comment/comment.model';

@Schema()
@ObjectType()
@Directive('@key(fields: "_id")')
export class File {
@Field(() => ID)
_id: mongoose.Schema.Types.ObjectId;

// File's UUID is generated by the s3 viewer plugin. It is stored in the S3 object's metadata.
// UUID is too long to put in MongoDB's _id field, so we store it in a separate field.
@Prop()
@Field()
fileId: string;

@Prop()
@Field()
bucket: string;

@Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }], default: [] })
@Field(() => [Comment], { nullable: true })
comments: mongoose.Schema.Types.ObjectId[];
}

export type FileDocument = File & Document;
export const FileSchema = SchemaFactory.createForClass(File);
18 changes: 18 additions & 0 deletions packages/server/src/file/file.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { File, FileSchema } from './file.model';
import { FileService } from './file.service';
import { FileResolver } from './file.resolver';
import { Comment, CommentSchema } from 'src/comment/comment.model';
import { CommentService } from 'src/comment/comment.service';

@Module({
imports: [
MongooseModule.forFeature([
{ name: File.name, schema: FileSchema },
{ name: Comment.name, schema: CommentSchema }
])
],
providers: [FileService, FileResolver, CommentService]
})
export class FileModule {}
34 changes: 34 additions & 0 deletions packages/server/src/file/file.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { FileService } from './file.service';
import { File } from './file.model';
import { CreateFileInput } from './file.dto';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt.guard';
import { Comment } from 'src/comment/comment.model';
import { CommentService } from 'src/comment/comment.service';

@Resolver(() => File)
@UseGuards(JwtAuthGuard)
export class FileResolver {
constructor(private fileService: FileService, private commentService: CommentService) {}

@Query(() => File, { nullable: true })
async getFileByFileId(@Args('fileId') fileId: string): Promise<File | null> {
return this.fileService.findByFileId(fileId);
}

@Mutation(() => File)
async addFile(@Args('input') input: CreateFileInput): Promise<File> {
return this.fileService.create(input);
}

@Mutation(() => Boolean)
async deleteFile(@Args('fileId') fileId: string): Promise<boolean> {
return this.fileService.removeFile(fileId);
}
wenhwang97 marked this conversation as resolved.
Show resolved Hide resolved

@ResolveField('comments', () => [Comment])
async getComments(@Parent() file: File): Promise<Comment[]> {
return this.commentService.findByFile(file.fileId);
}
}
29 changes: 29 additions & 0 deletions packages/server/src/file/file.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { File, FileDocument } from './file.model';
import { Model } from 'mongoose';
import { CreateFileInput } from './file.dto';
import { Comment, CommentDocument } from 'src/comment/comment.model';

@Injectable()
export class FileService {
constructor(
@InjectModel(File.name) private fileModel: Model<FileDocument>,
@InjectModel(Comment.name) private commentModel: Model<CommentDocument>
) {}

async create(createFileInput: CreateFileInput): Promise<File> {
const createFile = new this.fileModel(createFileInput);
return createFile.save();
}

findByFileId(id: string): Promise<File | null> {
return this.fileModel.findOne({ fileId: id }).exec();
}

async removeFile(id: string): Promise<boolean> {
await this.commentModel.deleteMany({ fileId: id }).exec();
const deleted = await this.fileModel.findOneAndDelete({ fileId: id }).exec();
return !!deleted;
}
}