Skip to content
Draft
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
88 changes: 87 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@opentelemetry/exporter-metrics-otlp-proto": "^0.56.0",
"@opentelemetry/host-metrics": "^0.35.5",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@pulsecron/pulse": "^1.6.7",
"@smithy/node-http-handler": "^3.1.2",
"@types/express-session": "^1.18.1",
"app-root-path": "^3.1.0",
Expand Down Expand Up @@ -88,9 +89,9 @@
"@types/mjml": "^4.7.4",
"@types/mongoose-delete": "^1.0.6",
"@types/morgan": "^1.9.9",
"@types/semver": "^7.5.8",
"@types/node": "^22.10.3",
"@types/nodemailer": "^6.4.17",
"@types/semver": "^7.5.8",
"@types/shelljs": "^0.8.15",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
Expand Down
15 changes: 15 additions & 0 deletions backend/src/services/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { fromEntity, toEntity } from '../utils/entity.js'
import { BadReq, Forbidden, InternalError, NotFound } from '../utils/error.js'
import { convertStringToId } from '../utils/id.js'
import { authResponseToUserPermission } from '../utils/permissions.js'
import { scheduleReview } from './scheduler.js'
import { getSchemaById } from './schema.js'

export function checkModelRestriction(model: ModelInterface) {
Expand Down Expand Up @@ -71,6 +72,7 @@ export async function createModel(user: UserInterface, modelParams: CreateModelP

await model.save()

await scheduleReview(model.id)
return model
}

Expand Down Expand Up @@ -567,3 +569,16 @@ export async function getCurrentUserPermissionsByModel(
exportMirroredModel: authResponseToUserPermission(exportMirroredModelAuth),
}
}

export async function getModelByIdForReview(modelId: string, kind?: EntryKindKeys) {
const model = await Model.findOne({
id: modelId,
...(kind && { kind }),
})

if (!model) {
throw NotFound(`The requested entry was not found.`, { modelId })
}

return model
}
22 changes: 21 additions & 1 deletion backend/src/services/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import { ReviewKind, ReviewKindKeys } from '../types/enums.js'
import { BadReq, InternalError, NotFound } from '../utils/error.js'
import log from './log.js'
import { getModelById } from './model.js'
import { requestReviewForAccessRequest, requestReviewForRelease } from './smtp/smtp.js'
import { requestReviewForAccessRequest, requestReviewForModel, requestReviewForRelease } from './smtp/smtp.js'

// This should be replaced by using the dynamic schema
const requiredRoles = {
release: ['mtr', 'msro'],
accessRequest: ['msro'],
model: ['msro'],
}

export async function findReviews(
Expand Down Expand Up @@ -49,6 +50,25 @@ export async function findReviews(
return reviews.filter((_, i) => auths[i].success)
}

export async function createModelReviews(model: ModelDoc) {
const roleEntities = getRoleEntities(requiredRoles.model, model.collaborators)

const createReviews = roleEntities.map((roleInfo) => {
const review = new Review({
modelId: model.id,
kind: ReviewKind.Model,
role: roleInfo.role,
})
roleInfo.entities.forEach((entity) =>
requestReviewForModel(entity, review, model).catch((error) =>
log.warn({ error }, 'Error when sending notifications requesting review for release.'),
),
)
return review.save()
})
await Promise.all(createReviews)
}

export async function createReleaseReviews(model: ModelDoc, release: ReleaseDoc) {
const roleEntities = getRoleEntities(requiredRoles.release, model.collaborators)

Expand Down
37 changes: 37 additions & 0 deletions backend/src/services/scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Pulse } from '@pulsecron/pulse'

import log from './log.js'
import { getModelByIdForReview } from './model.js'
import { createModelReviews } from './review.js'

const mongoConnectionString = 'mongodb://mongo:27017/pulse'

const pulse = new Pulse({
db: { address: mongoConnectionString },
defaultConcurrency: 4,
maxConcurrency: 4,
processEvery: '10 seconds',
resumeOnRestart: true,
})

pulse.on('start', (job) => {
log.info(`Job <${job.attrs.name}> starting`)
})
pulse.on('success', (job) => {
log.info(`Job <${job.attrs.name}> succeeded`)
})
pulse.on('fail', (error, job) => {
log.info(`Job <${job.attrs.name}> failed:`, error)
})

pulse.define('create model review', async (job) => {
const { modelId } = job.attrs.data

const model = await getModelByIdForReview(modelId)
await createModelReviews(model)
})

export async function scheduleReview(modelId: string) {
await pulse.start()
await pulse.schedule('in 2 minutes', 'create model review', { modelId })
}
22 changes: 21 additions & 1 deletion backend/src/services/smtp/smtp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Mail from 'nodemailer/lib/mailer/index.js'

import authentication from '../../connectors/authentication/index.js'
import { AccessRequestDoc } from '../../models/AccessRequest.js'
import { ModelDoc } from '../../models/Model.js'
import { ReleaseDoc } from '../../models/Release.js'
import { ResponseInterface } from '../../models/Response.js'
import { ReviewDoc } from '../../models/Review.js'
Expand All @@ -11,6 +12,7 @@ import { toEntity } from '../../utils/entity.js'
import log from '../log.js'
import { buildEmail, EmailContent } from './emailBuilder.js'

const appBaseUrl = `${config.app.protocol}://${config.app.host}:${config.app.port}`
let transporter: undefined | Transporter = undefined

async function dispatchEmail(entity: string, emailContent: EmailContent) {
Expand All @@ -29,7 +31,25 @@ async function dispatchEmail(entity: string, emailContent: EmailContent) {
await Promise.all(sendEmailResponses)
}

const appBaseUrl = `${config.app.protocol}://${config.app.host}:${config.app.port}`
export async function requestReviewForModel(entity: string, review: ReviewDoc, model: ModelDoc) {
if (!config.smtp.enabled) {
log.info('Not sending email due to SMTP disabled')
return
}

const emailContent = buildEmail(
`Model ${model.id} is ready for your review`,
[
{ title: 'Model ID', data: model.id },
{ title: 'Your Role', data: review.role.toUpperCase() },
],
[{ name: 'See Reviews', url: `${appBaseUrl}/review` }],
true,
)

await dispatchEmail(entity, emailContent)
}

export async function requestReviewForRelease(entity: string, review: ReviewDoc, release: ReleaseDoc) {
if (!config.smtp.enabled) {
log.info('Not sending email due to SMTP disabled')
Expand Down
Loading