Skip to content

feat(bulk-backend): Job and JobItem DB model, repository and service #2007

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

Merged
merged 27 commits into from
Oct 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
58a5429
Feat/link audit to include tags (#1950)
thanhdatle Sep 16, 2022
2a6385e
feat: search for tags in lower cap (#1973)
thanhdatle Sep 19, 2022
afaccb3
feat(tagging): add tags to link audit history (#1974)
halfwhole Sep 19, 2022
50b4959
chore(datadog): refactor custom metric names (#1965)
halfwhole Sep 19, 2022
e5e8bd1
feat(tagging): add frontend tagging on create new link form (#1919)
halfwhole Sep 19, 2022
4eff871
feat(tagging): add dropdown for tags on user page search bar (#1934)
halfwhole Sep 19, 2022
9496e2e
build(deps): bump winston from 3.3.3 to 3.8.1 (#1889)
halfwhole Sep 20, 2022
81bbb6e
build(deps): bump express-fileupload from 1.2.1 to 1.4.0 (#1890)
halfwhole Sep 20, 2022
212b235
feat(tagging): add frontend API integrations on create new link form …
halfwhole Sep 21, 2022
ec133c5
Feat/add tag to link audit frontend (#1975)
thanhdatle Sep 26, 2022
b0263cf
feat(tagging): add editing of link tags (#1976)
halfwhole Sep 26, 2022
7d04194
fix: return successful response when no URLs are found (#1979)
halfwhole Sep 27, 2022
d7293a8
fix(tagging): reset tags state after link creation (#1982)
halfwhole Sep 28, 2022
0eecef1
feat(tagging): add tags to links on user page (#1972)
halfwhole Sep 28, 2022
0a4a94e
fix: revise findUrlsForUser to return urls in correct order (#1981)
thanhdatle Sep 28, 2022
3198a0d
fix: fix urlMapper to correctly check for empty tagStrings before par…
thanhdatle Sep 28, 2022
2657f95
chore: remove disallowed file extensions (#1985)
halfwhole Sep 28, 2022
65152df
Fix/link tagging for file upload (#1986)
jimvae Sep 28, 2022
53e1205
fix: wrap backend errors in json messages (#1991)
halfwhole Sep 29, 2022
2947744
fix(tagging): serialize single tags for file uploads (#1990)
halfwhole Sep 29, 2022
89bc884
feat: add job and job item models
Oct 4, 2022
0ad3a00
feat: add jobmanagementservice
Oct 5, 2022
bc2d008
Merge branch 'develop' into feat/bulk-backend/job-v2
jimvae Oct 7, 2022
760ce88
chore: fix conflicts
Oct 7, 2022
2d27127
chore: rename JobStatusEnum to JobItemStatusEnum
Oct 16, 2022
922792c
chore: remove JobItemStatusEnum - Ready
Oct 16, 2022
c98a664
chore: replaced isSuccess to getJobStatus in JobManagementService
Oct 16, 2022
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
7 changes: 6 additions & 1 deletion src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ export const DependencyIds = {
userMapper: Symbol.for('userMapper'),
otpMapper: Symbol.for('otpMapper'),
tagMapper: Symbol.for('tagMapper'),
jobMapper: Symbol.for('jobMapper'),
jobItemMapper: Symbol.for('jobItemMapper'),
safeBrowsingMapper: Symbol.for('safeBrowsingMapper'),
analyticsLoggerService: Symbol.for('analyticsLoggerService'),
cookieReducer: Symbol.for('cookieReducer'),
userRepository: Symbol.for('userRepository'),
tagRepository: Symbol.for('tagRepository'),
jobRepository: Symbol.for('jobRepository'),
jobItemRepository: Symbol.for('jobItemRepository'),
otpRepository: Symbol.for('otpRepository'),
mailer: Symbol.for('mailer'),
cryptography: Symbol.for('cryptography'),
Expand Down Expand Up @@ -55,9 +59,10 @@ export const DependencyIds = {
linksToRotate: Symbol.for('linksToRotate'),
ogUrl: Symbol.for('ogUrl'),
gaTrackingId: Symbol.for('gaTrackingId'),
tagManagementService: Symbol.for('tagManagementService'),
jobManagementService: Symbol.for('jobManagementService'),
bulkController: Symbol.for('bulkController'),
bulkService: Symbol.for('bulkService'),
tagManagementService: Symbol.for('tagManagementService'),
}

export const ERROR_404_PATH = '404.error.ejs'
11 changes: 10 additions & 1 deletion src/server/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import { S3ServerSide } from './services/aws'
import { UrlRepository } from './repositories/UrlRepository'
import { UserRepository } from './repositories/UserRepository'
import { TagRepository } from './repositories/TagRepository'
import { JobRepository } from './repositories/JobRepository'
import { JobItemRepository } from './repositories/JobItemRepository'
import { UrlMapper } from './mappers/UrlMapper'
import { UserMapper } from './mappers/UserMapper'
import { OtpMapper } from './mappers/OtpMapper'
import { TagMapper } from './mappers/TagMapper'
import { JobItemMapper } from './mappers/JobItemMapper'
import { JobMapper } from './mappers/JobMapper'
import {
AnalyticsLoggerService,
CookieArrayReducerService,
Expand Down Expand Up @@ -72,7 +76,7 @@ import { FileCheckController, UrlCheckController } from './modules/threat'
import { QrCodeService } from './modules/qr/services'
import { QrCodeController } from './modules/qr'
import TagManagementService from './modules/user/services/TagManagementService'

import JobManagementService from './modules/job/services/JobManagementService'
import { BulkService } from './modules/bulk/services'
import { BulkController } from './modules/bulk'

Expand All @@ -99,11 +103,15 @@ export default () => {
bindIfUnbound(DependencyIds.userMapper, UserMapper)
bindIfUnbound(DependencyIds.otpMapper, OtpMapper)
bindIfUnbound(DependencyIds.tagMapper, TagMapper)
bindIfUnbound(DependencyIds.jobMapper, JobMapper)
bindIfUnbound(DependencyIds.jobItemMapper, JobItemMapper)
bindIfUnbound(DependencyIds.analyticsLoggerService, AnalyticsLoggerService)
bindIfUnbound(DependencyIds.cookieReducer, CookieArrayReducerService)
bindIfUnbound(DependencyIds.otpRepository, OtpRepository)
bindIfUnbound(DependencyIds.userRepository, UserRepository)
bindIfUnbound(DependencyIds.tagRepository, TagRepository)
bindIfUnbound(DependencyIds.jobRepository, JobRepository)
bindIfUnbound(DependencyIds.jobItemRepository, JobItemRepository)
bindIfUnbound(DependencyIds.cryptography, CryptographyBcrypt)
bindIfUnbound(DependencyIds.redirectController, RedirectController)
bindIfUnbound(DependencyIds.gaController, GaController)
Expand All @@ -119,6 +127,7 @@ export default () => {
bindIfUnbound(DependencyIds.logoutController, LogoutController)
bindIfUnbound(DependencyIds.urlManagementService, UrlManagementService)
bindIfUnbound(DependencyIds.tagManagementService, TagManagementService)
bindIfUnbound(DependencyIds.jobManagementService, JobManagementService)
bindIfUnbound(DependencyIds.userController, UserController)
bindIfUnbound(DependencyIds.qrCodeService, QrCodeService)
bindIfUnbound(DependencyIds.qrCodeController, QrCodeController)
Expand Down
25 changes: 25 additions & 0 deletions src/server/mappers/JobItemMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable class-methods-use-this, lines-between-class-members, no-dupe-class-members */
import { injectable } from 'inversify'
import { StorableJobItem } from '../repositories/types'
import { JobItemType } from '../models/job'
import { Mapper } from './Mapper'

@injectable()
export class JobItemMapper implements Mapper<StorableJobItem, JobItemType> {
persistenceToDto(jobItemType: JobItemType): StorableJobItem
persistenceToDto(jobItemType: JobItemType | null): StorableJobItem | null {
if (!jobItemType) {
return null
}
return {
id: jobItemType.id,
status: jobItemType.status,
message: jobItemType.message,
type: jobItemType.type,
jobId: jobItemType.jobId,
params: jobItemType.params,
}
}
}

export default JobItemMapper
21 changes: 21 additions & 0 deletions src/server/mappers/JobMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable class-methods-use-this, lines-between-class-members, no-dupe-class-members */
import { injectable } from 'inversify'
import { StorableJob } from '../repositories/types'
import { JobType } from '../models/job'
import { Mapper } from './Mapper'

@injectable()
export class JobMapper implements Mapper<StorableJob, JobType> {
persistenceToDto(jobType: JobType): StorableJob
persistenceToDto(jobType: JobType | null): StorableJob | null {
if (!jobType) {
return null
}
return {
uuid: jobType.uuid,
id: jobType.id,
}
}
}

export default JobMapper
9 changes: 9 additions & 0 deletions src/server/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ import { Devices } from './statistics/devices'
import { UrlClicks } from './statistics/clicks'
import { syncFunctions } from './functions'
import { Tag } from './tag'
import { Job, JobItem } from './job'

// One user can create many urls but each url can only be mapped to one user.
User.hasMany(Url, { as: 'Urls', foreignKey: { allowNull: false } })
Url.belongsTo(User, { foreignKey: { allowNull: false } })

// One user can run many jobs but each job can only be mapped to one user.
User.hasMany(Job, { as: 'Job', foreignKey: { allowNull: false } })
Job.belongsTo(User, { foreignKey: { allowNull: false } })

// One job can run many jobItems but each jobItem can only be mapped to one job.
Job.hasMany(JobItem, { as: 'JobItem', foreignKey: { allowNull: false } })
JobItem.belongsTo(Job, { foreignKey: { allowNull: false } })

export const UrlTag = sequelize.define('url_tag', {}, { timestamps: true })

// An Url has many to many mapping to Tag
Expand Down
80 changes: 80 additions & 0 deletions src/server/models/job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Sequelize from 'sequelize'
import { IdType } from '../../types/server/models'
import { JobItemStatusEnum, JobTypeEnum } from '../repositories/enums'
import { sequelize } from '../util/sequelize'

export interface JobType extends IdType, Sequelize.Model {
readonly uuid: string
readonly userId: Number
readonly createdAt: string
readonly updatedAt: string
}

type JobStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): JobType
}

export const Job = <JobStatic>sequelize.define(
'job',
{
uuid: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
unique: true,
},
},
{
defaultScope: {
useMaster: true,
},
},
)

export interface JobItemType extends IdType, Sequelize.Model {
readonly status: JobItemStatusEnum
readonly message: string
readonly type: JobTypeEnum
readonly params: JSON
readonly jobId: Number
readonly createdAt: string
readonly updatedAt: string
}

type JobItemStatic = typeof Sequelize.Model & {
new (values?: object, options?: Sequelize.BuildOptions): JobItemType
}

export const JobItem = <JobItemStatic>sequelize.define(
'job_item',
{
status: {
type: Sequelize.ENUM,
values: [
JobItemStatusEnum.InProgress,
JobItemStatusEnum.Success,
JobItemStatusEnum.Failed,
],
defaultValue: JobItemStatusEnum.InProgress,
allowNull: false,
},
message: {
type: Sequelize.STRING,
defaultValue: '',
allowNull: false,
},
type: {
type: Sequelize.ENUM,
values: [JobTypeEnum.QRCodeGeneration],
allowNull: false,
},
params: {
type: Sequelize.JSON,
allowNull: false,
},
},
{
defaultScope: {
useMaster: true,
},
},
)
21 changes: 21 additions & 0 deletions src/server/modules/job/interfaces/JobManagementService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { JobItemStatusEnum, JobTypeEnum } from '../../../repositories/enums'
import { StorableJob, StorableJobItem } from '../../../repositories/types'

interface JobManagementServiceInterface {
createJob(userId: number): Promise<StorableJob>
findJobById(id: number): Promise<StorableJob | null>
createJobItem: (properties: {
status: JobItemStatusEnum
message: string
type: JobTypeEnum
params: JSON
jobId: number
}) => Promise<StorableJobItem>
updateJobItem(
jobItem: StorableJobItem,
changes: Partial<StorableJobItem>,
): Promise<StorableJobItem>
findJobItemsByJobId(jobId: number): Promise<StorableJobItem[]>
getJobStatus(jobId: number): Promise<JobItemStatusEnum>
}
export default JobManagementServiceInterface
109 changes: 109 additions & 0 deletions src/server/modules/job/services/JobManagementService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { inject, injectable } from 'inversify'
import { NotFoundError } from '../../../util/error'
import { JobItemRepositoryInterface } from '../../../repositories/interfaces/JobItemRepositoryInterface'
import { JobRepositoryInterface } from '../../../repositories/interfaces/JobRepositoryInterface'
import { DependencyIds } from '../../../constants'
import JobManagementServiceInterface from '../interfaces/JobManagementService'
import { StorableJob, StorableJobItem } from '../../../repositories/types'
import { JobItemStatusEnum, JobTypeEnum } from '../../../repositories/enums'
import { UserRepositoryInterface } from '../../../repositories/interfaces/UserRepositoryInterface'

@injectable()
class JobManagementService implements JobManagementServiceInterface {
private jobRepository: JobRepositoryInterface

private jobItemRepository: JobItemRepositoryInterface

private userRepository: UserRepositoryInterface

constructor(
@inject(DependencyIds.jobRepository) jobRepository: JobRepositoryInterface,
@inject(DependencyIds.jobItemRepository)
jobItemRepository: JobItemRepositoryInterface,
@inject(DependencyIds.userRepository)
userRepository: UserRepositoryInterface,
) {
this.jobRepository = jobRepository
this.jobItemRepository = jobItemRepository
this.userRepository = userRepository
}

createJob: (userId: number) => Promise<StorableJob> = async (userId) => {
const user = await this.userRepository.findById(userId)
if (!user) {
throw new NotFoundError('User not found')
}

const job = await this.jobRepository.create(userId)
return job
}

findJobById: (id: number) => Promise<StorableJob | null> = async (id) => {
const job = await this.jobRepository.findById(id)
return job
}

createJobItem: (properties: {
status: JobItemStatusEnum
message: string
type: JobTypeEnum
params: JSON
jobId: number
}) => Promise<StorableJobItem> = async (properties) => {
const jobItem = await this.jobItemRepository.create(properties)
return jobItem
}

updateJobItem: (
jobItem: StorableJobItem,
changes: Partial<StorableJobItem>,
) => Promise<StorableJobItem> = async (jobItem, changes) => {
const updatedItem = await this.jobItemRepository.update(jobItem, changes)
return updatedItem
}

findJobItemsByJobId: (jobId: number) => Promise<StorableJobItem[]> = async (
jobId,
) => {
const job = await this.jobRepository.findById(jobId)
if (!job) {
throw new NotFoundError('Job not found')
}

const jobItems = await this.jobItemRepository.findJobItemsByJobId(jobId)

return jobItems
}

// 'success' when all related job items are also successful
// 'failed' if at least one of them job item failed
// 'in progress' if at least one of the job item is in progress (while the rest are either in progress or successful)
getJobStatus: (jobId: number) => Promise<JobItemStatusEnum> = async (
jobId,
) => {
const jobItems = await this.findJobItemsByJobId(jobId)

if (jobItems.length === 0) {
throw new Error('Job does not have any job items')
}

let isInProgress = false

for (let i = 0; i < jobItems.length; i += 1) {
const { status } = jobItems[i]
if (status === JobItemStatusEnum.Failed) {
return JobItemStatusEnum.Failed
}
if (status === JobItemStatusEnum.InProgress) {
isInProgress = true
}
}

if (isInProgress) {
return JobItemStatusEnum.InProgress
}
return JobItemStatusEnum.Success
}
}

export default JobManagementService
Loading