Skip to content

Commit def048a

Browse files
author
aminsh
committed
feat(app): add graphql subscription
1 parent 7a9c6a6 commit def048a

18 files changed

+376
-217
lines changed

backend/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@
4343
"googleapis": "^140.0.0",
4444
"graphql": "^16.6.0",
4545
"graphql-scalars": "^1.21.3",
46+
"graphql-subscriptions": "^2.0.0",
4647
"graphql-type-json": "^0.3.2",
48+
"graphql-ws": "^5.16.0",
4749
"linq": "3.2.3",
4850
"md5": "^2.3.0",
4951
"mongoose": "^7.0.3",
5052
"passport-google-oauth20": "^2.0.0",
5153
"reflect-metadata": "^0.1.13",
5254
"rimraf": "^3.0.2",
5355
"rxjs": "^7.2.0",
54-
"socket.io": "^4.7.5"
56+
"socket.io": "^4.7.5",
57+
"subscriptions-transport-ws": "^0.11.0"
5558
},
5659
"devDependencies": {
5760
"@nestjs/cli": "^8.0.0",

backend/src/app.module.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@ import { AuthModule } from './auth/auth.module'
2020

2121
@Module({
2222
imports: [
23-
ConfigModule.forRoot({ isGlobal: true }),
23+
ConfigModule.forRoot({isGlobal: true}),
2424
MongooseModule.forRootAsync({
25-
imports: [ ConfigModule ],
26-
inject: [ ConfigService ],
27-
useFactory: (configService: ConfigService) => ({ uri: configService.get('MONGO_URI') })
25+
imports: [ConfigModule],
26+
inject: [ConfigService],
27+
useFactory: (configService: ConfigService) => ({uri: configService.get('MONGO_URI')})
2828
}),
2929
GraphQLModule.forRoot<ApolloDriverConfig>({
3030
driver: ApolloDriver,
3131
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
3232
resolvers: {
3333
Void: VoidResolver
34-
}
34+
},
35+
installSubscriptionHandlers: true,
36+
subscriptions: {
37+
'graphql-ws': true,
38+
'subscriptions-transport-ws': true,
39+
},
3540
}),
3641
ClientsModule.register([
3742
{name: MESSAGE_SERVICE, transport: Transport.TCP},
@@ -42,8 +47,8 @@ import { AuthModule } from './auth/auth.module'
4247
UserModule,
4348
AuthModule,
4449
],
45-
controllers: [ AppController ],
46-
providers: [ AppService ],
50+
controllers: [AppController],
51+
providers: [AppService],
4752
})
4853
export class AppModule {
49-
}
54+
}

backend/src/note/note.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MongooseModule } from '@nestjs/mongoose'
33
import { Note, NoteSchema } from './schema/note'
44
import { NoteService } from './service/note.service'
55
import { NoteRepository } from './repository/note.repository'
6-
import { NoteResolver } from './resolver/note.resolver'
6+
import { NoteResolver, NoteSubscriptionResolver } from './resolver/note.resolver'
77
import { NoteQueryService } from './service/note-query.service'
88
import { NoteMessageController } from './controller/note-message.controller'
99
import { ClientsModule, Transport } from '@nestjs/microservices'
@@ -25,6 +25,7 @@ import { MESSAGE_SERVICE } from '../shared/shared.contacts'
2525
NoteRepository,
2626
NoteService,
2727
NoteResolver,
28+
NoteSubscriptionResolver,
2829
NoteQueryService,
2930
],
3031
exports: [

backend/src/note/resolver/note.resolver.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { InjectModel } from '@nestjs/mongoose'
22
import { Note } from '../schema/note'
33
import { Model } from 'mongoose'
4-
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
4+
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'
55
import { NotePageableResponse, NoteView } from '../dto/note.view'
66
import { NoteService } from '../service/note.service'
77
import { NoteDto } from '../dto/note.dto'
@@ -12,6 +12,9 @@ import { noteAssembler } from '../dto/note-assembler'
1212
import { NpRequestContext } from '../../shared/service/np-request-context.service'
1313
import { NoteShareDTO } from '../dto/note-shared.dto'
1414
import { handleNoteFindRequest, NoteFindRequest } from '../dto/note-find.request'
15+
import { PubSub } from 'graphql-subscriptions'
16+
17+
const pubSub = new PubSub()
1518

1619
@UseGuards(JwtGqlAuthenticationGuard)
1720
@Resolver(() => NoteView)
@@ -47,8 +50,13 @@ export class NoteResolver {
4750

4851
@Mutation(() => NoteView, {name: 'noteCreate'})
4952
async create(@Args('input') dto: NoteDto): Promise<NoteView> {
50-
const result = await this.noteService.create(dto)
51-
return noteAssembler(result)
53+
const newNote = await this.noteService.create(dto)
54+
55+
const view = noteAssembler(newNote)
56+
57+
await pubSub.publish('noteCreated', {noteCreated: view})
58+
59+
return view
5260
}
5361

5462
@Mutation(() => VoidResolver, {
@@ -81,3 +89,13 @@ export class NoteResolver {
8189
return this.noteService.share(id, dto)
8290
}
8391
}
92+
93+
@Resolver(() => NoteView)
94+
export class NoteSubscriptionResolver {
95+
@Subscription(() => NoteView, {
96+
name: 'noteCreated'
97+
})
98+
noteCreated() {
99+
return pubSub.asyncIterator('noteCreated')
100+
}
101+
}

backend/src/schema.gql

+4
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,8 @@ input LoginDTO {
153153

154154
input UpdateUserDTO {
155155
name: String!
156+
}
157+
158+
type Subscription {
159+
noteCreated: NoteView!
156160
}

backend/yarn.lock

+14-2
Original file line numberDiff line numberDiff line change
@@ -4163,6 +4163,13 @@ graphql-scalars@^1.21.3:
41634163
dependencies:
41644164
tslib "^2.5.0"
41654165

4166+
graphql-subscriptions@^2.0.0:
4167+
version "2.0.0"
4168+
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-2.0.0.tgz#11ec181d475852d8aec879183e8e1eb94f2eb79a"
4169+
integrity sha512-s6k2b8mmt9gF9pEfkxsaO1lTxaySfKoEJzEfmwguBbQ//Oq23hIXCfR1hm4kdh5hnR20RdwB+s3BCb+0duHSZA==
4170+
dependencies:
4171+
iterall "^1.3.0"
4172+
41664173
[email protected], graphql-tag@^2.11.0:
41674174
version "2.12.6"
41684175
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
@@ -4185,6 +4192,11 @@ [email protected]:
41854192
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.12.0.tgz#d06fe38916334b4a4c827f73268cbf4359a32ed7"
41864193
integrity sha512-PA3ImUp8utrpEjoxBMhvxsjkStvFEdU0E1gEBREt8HZIWkxOUymwJBhFnBL7t/iHhUq1GVPeZevPinkZFENxTw==
41874194

4195+
graphql-ws@^5.16.0:
4196+
version "5.16.0"
4197+
resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.0.tgz#849efe02f384b4332109329be01d74c345842729"
4198+
integrity sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==
4199+
41884200
graphql@^16.6.0:
41894201
version "16.6.0"
41904202
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb"
@@ -4603,7 +4615,7 @@ istanbul-reports@^3.1.3:
46034615
html-escaper "^2.0.0"
46044616
istanbul-lib-report "^3.0.0"
46054617

4606-
[email protected], iterall@^1.2.1:
4618+
[email protected], iterall@^1.2.1, iterall@^1.3.0:
46074619
version "1.3.0"
46084620
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
46094621
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
@@ -7155,7 +7167,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
71557167
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
71567168
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
71577169

7158-
7170+
[email protected], subscriptions-transport-ws@^0.11.0:
71597171
version "0.11.0"
71607172
resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883"
71617173
integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ==

frontend/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"dependencies": {
66
"@ant-design/icons": "^5.0.1",
7-
"@apollo/client": "^3.7.12",
7+
"@apollo/client": "^3.11.4",
88
"@ckeditor/ckeditor5-build-classic": "^39.0.2",
99
"@ckeditor/ckeditor5-react": "^6.1.0",
1010
"@reduxjs/toolkit": "^1.9.5",
@@ -17,7 +17,8 @@
1717
"@types/react-dom": "^18.0.11",
1818
"antd": "^5.4.4",
1919
"bootstrap": "^5.2.3",
20-
"graphql": "^16.6.0",
20+
"graphql": "^16.9.0",
21+
"graphql-ws": "^5.16.0",
2122
"moment": "^2.30.1",
2223
"react": "^18.2.0",
2324
"react-dom": "^18.2.0",
@@ -27,7 +28,7 @@
2728
"react-scripts": "5.0.1",
2829
"sanitize-html": "^2.13.0",
2930
"sass": "^1.62.0",
30-
"socket.io-client": "^4.7.5",
31+
"subscriptions-transport-ws": "^0.11.0",
3132
"typescript": "^4.9.5",
3233
"web-vitals": "^2.1.4"
3334
},

frontend/src/component/File/FileTypeIcon.tsx

+38-3
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ import SvgDoc from '../../asset/docx.svg'
88
import SvgTxt from '../../asset/txt.svg'
99
import { Image } from 'antd'
1010
import { resolvePathFile } from '../../utils'
11+
import * as docxPreview from 'docx-preview'
1112

12-
const fileTypeMapperFactory = (size: number): Record<FileType, React.ReactNode> => {
13+
const fileTypeMapperFactory = (size: number, file?: File): Record<FileType, React.ReactNode> => {
1314
return {
1415
[FileType.JPG]: <FileJpgOutlined style={{fontSize: size}}/>,
1516
[FileType.PNG]: <FileImageOutlined style={{fontSize: size}}/>,
1617
[FileType.PDF]: <FileIcon size={size} src={SvgPdf} alt="pdf"/>,
1718
[FileType.XLS]: <FileIcon size={size} src={SvgXls} alt="xls"/>,
18-
[FileType.DOC]: <FileIcon size={size} src={SvgDoc} alt="doc"/>,
19+
[FileType.DOC]: <DocIcon size={size} file={file}/>,
1920
[FileType.TXT]: <FileIcon size={size} src={SvgTxt} alt="txt"/>,
2021
}
2122
}
2223

2324
export const FileTypeIcon = ({file, size}: { file: File, size: number }) => {
24-
const fileTypeMapper = fileTypeMapperFactory(size)
25+
const fileTypeMapper = fileTypeMapperFactory(size, file)
2526

2627
return (<>
2728
{
@@ -33,4 +34,38 @@ export const FileTypeIcon = ({file, size}: { file: File, size: number }) => {
3334
: fileTypeMapper[file.type]
3435
}
3536
</>)
37+
}
38+
39+
export const DocIcon = ({ file, size }: { file?: File, size: number }) => {
40+
const handleClick = () => {
41+
if(!file)
42+
return
43+
44+
fetch(resolvePathFile(file.filename))
45+
.then(async res => {
46+
const blob = await res.blob()
47+
48+
await docxPreview.renderAsync(
49+
blob,
50+
// @ts-ignore
51+
document.getElementById('docx-container'),
52+
)
53+
})
54+
}
55+
return(
56+
<>
57+
<Image
58+
width={35}
59+
src={SvgDoc}
60+
preview={{
61+
destroyOnClose: true,
62+
imageRender: () => {
63+
handleClick()
64+
return <div id='docx-container'></div>
65+
},
66+
toolbarRender: () => null,
67+
}}
68+
/>
69+
</>
70+
)
3671
}
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,26 @@
11
import { Outlet } from 'react-router-dom'
2-
import { useAuth } from '../../hook/auth.hook'
3-
import { useEffect, useState } from 'react'
42
import { Layout, Menu } from 'antd'
5-
import { Socket } from 'socket.io-client'
6-
import { configure } from '../../config/socket-client'
7-
import { SocketContext } from '../../socket'
83
import style from './ProtectedLayout.module.scss'
94
import { menuItems } from '../../config/menuItems'
105

116
const {Content, Sider} = Layout
127

138
export const ProtectedLayout = () => {
14-
const auth = useAuth()
15-
const [socket, setSocket] = useState<Socket>()
16-
17-
const startSocket = async () => {
18-
const client = configure()
19-
setSocket(client)
20-
}
21-
22-
useEffect(() => {
23-
auth.validate()
24-
startSocket()
25-
26-
return () => {
27-
socket?.close()
28-
}
29-
}, [])
30-
319
return (
32-
<SocketContext.Provider value={{socket}}>
33-
<Layout className='bg-white'>
34-
<Sider style={{background: 'transparent'}}>
35-
<Menu
36-
style={{ height: '100vh' }}
37-
mode='inline'
38-
inlineCollapsed={ false }
39-
items={menuItems}
40-
/>
41-
</Sider>
42-
<Layout className={style.mainLayout}>
43-
<Content>
44-
<Outlet/>
45-
</Content>
46-
</Layout>
10+
<Layout className='bg-white'>
11+
<Sider style={{background: 'transparent'}}>
12+
<Menu
13+
style={{ height: '100vh' }}
14+
mode='inline'
15+
inlineCollapsed={ false }
16+
items={menuItems}
17+
/>
18+
</Sider>
19+
<Layout className={style.mainLayout}>
20+
<Content>
21+
<Outlet/>
22+
</Content>
4723
</Layout>
48-
</SocketContext.Provider>
24+
</Layout>
4925
)
5026
}

0 commit comments

Comments
 (0)