Skip to content

Commit 8c13981

Browse files
committed
Implemented conversation model, repo, and controller
1 parent 6305cf5 commit 8c13981

File tree

6 files changed

+315
-0
lines changed

6 files changed

+315
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
const mongoose = require('mongoose')
2+
const logger = require('../../middleware/logger')
3+
const getConstants = require('../../../src/constants').getConstants
4+
const CONSTANTS = getConstants()
5+
6+
async function getAllConversations (req, res, next) {
7+
const repo = req.ctx.repositories.getConversationRepository()
8+
9+
// temporary measure to allow tests to work after fixing #920
10+
// tests required changing the global limit to force pagination
11+
if (req.TEST_PAGINATOR_LIMIT) {
12+
CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
13+
}
14+
15+
const options = CONSTANTS.PAGINATOR_OPTIONS
16+
options.sort = { posted_at: 'desc' }
17+
18+
const response = await repo.getAll(options)
19+
return res.status(200).json(response)
20+
}
21+
22+
async function getConversationsForOrg (req, res, next) {
23+
const session = await mongoose.startSession()
24+
25+
try {
26+
session.startTransaction()
27+
28+
const repo = req.ctx.repositories.getConversationRepository()
29+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
30+
const requesterOrg = req.ctx.org
31+
const targetOrgUUID = req.params.uuid
32+
33+
// Make sure target org matches user org if not secretariat
34+
const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session })
35+
const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session })
36+
if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) {
37+
return res.status(400).json({ message: 'User is not secretariat or admin for target org' })
38+
}
39+
40+
// temporary measure to allow tests to work after fixing #920
41+
// tests required changing the global limit to force pagination
42+
if (req.TEST_PAGINATOR_LIMIT) {
43+
CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
44+
}
45+
46+
const options = CONSTANTS.PAGINATOR_OPTIONS
47+
options.sort = { posted_at: 'desc' }
48+
49+
const response = await repo.getAllByTargetUUID(targetOrgUUID, options)
50+
await session.commitTransaction()
51+
return res.status(200).json(response)
52+
} catch (err) {
53+
if (session && session.inTransaction()) {
54+
await session.abortTransaction()
55+
}
56+
next(err)
57+
} finally {
58+
if (session && session.id) { // Check if session is still valid before trying to end
59+
try {
60+
await session.endSession()
61+
} catch (sessionEndError) {
62+
logger.error({ uuid: req.ctx.uuid, message: 'Error ending session in finally block', error: sessionEndError })
63+
}
64+
}
65+
}
66+
}
67+
68+
async function createConversationForOrg (req, res, next) {
69+
const session = await mongoose.startSession()
70+
71+
try {
72+
session.startTransaction()
73+
74+
const repo = req.ctx.repositories.getConversationRepository()
75+
const orgRepo = req.ctx.repositories.getBaseOrgRepository()
76+
const userRepo = req.ctx.repositories.getBaseUserRepository()
77+
const requesterOrg = req.ctx.org
78+
const requesterUsername = req.ctx.user
79+
const targetOrgUUID = req.params.uuid
80+
const body = req.body
81+
82+
// Make sure target org matches user org if not secretariat
83+
const isSecretariat = await orgRepo.isSecretariatByShortName(requesterOrg, { session })
84+
const requesterOrgUUID = await orgRepo.getOrgUUID(requesterOrg, { session })
85+
if (!isSecretariat && (requesterOrgUUID !== targetOrgUUID)) {
86+
return res.status(400).json({ message: 'User is not secretariat or admin for target org' })
87+
}
88+
89+
const user = await userRepo.findOneByUsernameAndOrgShortname(requesterUsername, requesterOrg, { session })
90+
91+
if (!body.body) {
92+
return res.status(400).json({ message: 'Missing required field body' })
93+
}
94+
95+
const conversationBody = {
96+
target_uuid: targetOrgUUID,
97+
author_id: user.UUID,
98+
author_name: [user.name.first, user.name.last].join(' '),
99+
author_role: isSecretariat ? 'Secretariat' : 'Partner',
100+
visibility: body.visibility ? body.visibility.toLowerCase() : 'private',
101+
body: body.body
102+
}
103+
const result = await repo.createConversation(conversationBody, { session })
104+
await session.commitTransaction()
105+
if (!result) {
106+
return res.status(500).json({ message: 'Failed to create conversation' })
107+
}
108+
return res.status(200).json(result)
109+
} catch (err) {
110+
if (session && session.inTransaction()) {
111+
await session.abortTransaction()
112+
}
113+
next(err)
114+
} finally {
115+
if (session && session.id) {
116+
// Check if session is still valid before trying to end
117+
try {
118+
await session.endSession()
119+
} catch (sessionEndError) {
120+
logger.error({
121+
uuid: req.ctx.uuid,
122+
message: 'Error ending session in finally block',
123+
error: sessionEndError
124+
})
125+
}
126+
}
127+
}
128+
}
129+
130+
async function updateMessage (req, res, next) {
131+
const repo = req.ctx.repositories.getConversationRepository()
132+
const targetOrgUUID = req.params.uuid
133+
const body = req.body
134+
135+
if (!body.body) {
136+
return res.status(400).json({ message: 'Missing required field body' })
137+
}
138+
139+
const result = await repo.updateConversation(body, targetOrgUUID)
140+
return res.status(200).json(result)
141+
}
142+
143+
module.exports = {
144+
getAllConversations,
145+
getConversationsForOrg,
146+
createConversationForOrg,
147+
updateMessage
148+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const router = require('express').Router()
2+
const { param, query } = require('express-validator')
3+
const controller = require('./conversation.controller')
4+
const mw = require('../../middleware/middleware')
5+
const getConstants = require('../../../src/constants').getConstants
6+
const CONSTANTS = getConstants()
7+
8+
// Get all conversations - SEC only
9+
router.get('/conversation',
10+
mw.validateUser,
11+
mw.onlySecretariat,
12+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }),
13+
query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
14+
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
15+
controller.getAllConversations
16+
)
17+
18+
// Get conversations for all orgs - SEC only
19+
router.get('/conversation/org',
20+
mw.validateUser,
21+
mw.onlySecretariat,
22+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }),
23+
query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
24+
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
25+
controller.getAllConversations // TODO: for now, all conversations are targeted to orgs. Update this when conversations added for other objects
26+
)
27+
28+
// Get conversations for org - SEC/ADMIN
29+
router.get('/conversation/org/:uuid',
30+
mw.validateUser,
31+
mw.onlySecretariatOrAdmin,
32+
query().custom((query) => { return mw.validateQueryParameterNames(query, ['page']) }),
33+
query(['page']).custom((val) => { return mw.containsNoInvalidCharacters(val) }),
34+
query(['page']).optional().isInt({ min: CONSTANTS.PAGINATOR_PAGE }),
35+
param(['uuid']).isUUID(4),
36+
controller.getConversationsForOrg
37+
)
38+
39+
// Post conversation for org - SEC/ADMIN
40+
router.post('/conversation/org/:uuid',
41+
mw.validateUser,
42+
mw.onlySecretariatOrAdmin,
43+
param(['uuid']).isUUID(4),
44+
controller.createConversationForOrg
45+
)
46+
47+
// Update conversation message - SEC only
48+
router.put('/conversation/:uuid/message',
49+
mw.validateUser,
50+
mw.onlySecretariat,
51+
param(['uuid']).isUUID(4),
52+
controller.updateMessage
53+
)
54+
55+
module.exports = router

src/model/conversation.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const mongoose = require('mongoose')
2+
const aggregatePaginate = require('mongoose-aggregate-paginate-v2')
3+
const MongoPaging = require('mongo-cursor-pagination')
4+
5+
const schema = {
6+
UUID: String,
7+
target_uuid: String,
8+
author_id: String,
9+
author_name: String,
10+
author_role: String,
11+
visibility: String,
12+
body: String,
13+
posted_at: Date
14+
}
15+
16+
const ConversationSchema = new mongoose.Schema(schema, { collection: 'Conversation', timestamps: { createdAt: 'posted_at', updatedAt: 'last_updated' } })
17+
18+
ConversationSchema.index({ target_uuid: 1 })
19+
ConversationSchema.index({ author_id: 1 })
20+
ConversationSchema.index({ posted_at: 1 })
21+
22+
ConversationSchema.plugin(aggregatePaginate)
23+
24+
// Cursor pagination
25+
ConversationSchema.plugin(MongoPaging.mongoosePlugin)
26+
const Conversation = mongoose.model('Conversation', ConversationSchema)
27+
module.exports = Conversation
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const uuid = require('uuid')
2+
const ConversationModel = require('../model/conversation')
3+
const BaseRepository = require('./baseRepository')
4+
5+
class ConversationRepository extends BaseRepository {
6+
constructor () {
7+
super(ConversationModel)
8+
}
9+
10+
async findOneByUUID (UUID, options = {}) {
11+
const result = await ConversationModel.findOne(
12+
{ UUID: UUID },
13+
null,
14+
options
15+
)
16+
return result || null
17+
}
18+
19+
async getAll (options = {}) {
20+
const agt = [
21+
{
22+
$match: {}
23+
}
24+
]
25+
const pg = await this.aggregatePaginate(agt, options)
26+
const data = { conversations: pg.itemsList }
27+
if (pg.itemCount >= options.limit) {
28+
data.totalCount = pg.itemCount
29+
data.itemsPerPage = pg.itemsPerPage
30+
data.pageCount = pg.pageCount
31+
data.currentPage = pg.currentPage
32+
data.prevPage = pg.prevPage
33+
data.nextPage = pg.nextPage
34+
}
35+
return data
36+
}
37+
38+
async getAllByTargetUUID (targetUUID, options = {}) {
39+
const agt = [
40+
{
41+
$match: {
42+
target_uuid: targetUUID
43+
}
44+
}
45+
]
46+
const pg = await this.aggregatePaginate(agt, options)
47+
const data = { conversations: pg.itemsList }
48+
if (pg.itemCount >= options.limit) {
49+
data.totalCount = pg.itemCount
50+
data.itemsPerPage = pg.itemsPerPage
51+
data.pageCount = pg.pageCount
52+
data.currentPage = pg.currentPage
53+
data.prevPage = pg.prevPage
54+
data.nextPage = pg.nextPage
55+
}
56+
return data
57+
}
58+
59+
async createConversation (body, options = {}) {
60+
body.UUID = uuid.v4()
61+
const newConversation = new ConversationModel(body)
62+
const result = await newConversation.save(options)
63+
return result.toObject()
64+
}
65+
66+
async updateConversation (body, UUID, options = {}) {
67+
const conversation = await this.findOneByUUID(UUID)
68+
69+
// Only allow updates to message body for now
70+
conversation.body = body.body
71+
72+
const result = await conversation.save(options)
73+
return result.toObject()
74+
}
75+
}
76+
77+
module.exports = ConversationRepository

src/repositories/repositoryFactory.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const RegistryUserRepository = require('./registryUserRepository')
77
const RegistryOrgRepository = require('./registryOrgRepository')
88
const BaseOrgRepository = require('./baseOrgRepository')
99
const BaseUserRepository = require('./baseUserRepository')
10+
const ConversationRepository = require('./conversationRepository')
1011

1112
class RepositoryFactory {
1213
getOrgRepository () {
@@ -53,6 +54,11 @@ class RepositoryFactory {
5354
const repo = new BaseUserRepository()
5455
return repo
5556
}
57+
58+
getConversationRepository () {
59+
const repo = new ConversationRepository()
60+
return repo
61+
}
5662
}
5763

5864
module.exports = RepositoryFactory

src/routes.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const SystemController = require('./controller/system.controller')
99
const UserController = require('./controller/user.controller')
1010
const RegistryUserController = require('./controller/registry-user.controller')
1111
const RegistryOrgController = require('./controller/registry-org.controller')
12+
const ConversationController = require('./controller/conversation.controller')
1213

1314
var options = {
1415
swaggerOptions: {
@@ -34,6 +35,7 @@ module.exports = async function configureRoutes (app) {
3435
app.use('/api/', UserController)
3536
app.use('/api/', RegistryUserController)
3637
app.use('/api/', RegistryOrgController)
38+
app.use('/api/', ConversationController)
3739
app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification))
3840
app.use('/api-docs', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, setupOptions))
3941
app.use('/schemas/', SchemasController)

0 commit comments

Comments
 (0)