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
51 changes: 51 additions & 0 deletions src/api/forms/service/callSessionTransaction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js'
import { logger, partialAuditFields } from '~/src/api/forms/service/shared.js'
import { client } from '~/src/mongo.js'

/**
* Abstraction of a generic service method
* @template T
* @param {string} formId
* @param {(session: ClientSession) => Promise<T>} asyncHandler
* @param {FormMetadataAuthor} author
* @param {{ start: string; end: string; fail: string }} logs
* @returns {Promise<T>}
*/
export async function callSessionTransaction(
formId,
asyncHandler,
author,
logs
) {
logger.info(logs.start)

const session = client.startSession()

try {
const sessionReturn = await session.withTransaction(async () => {
const handlerReturn = await asyncHandler(session)

// Update the form with the new draft state
await formMetadata.update(
formId,
{ $set: partialAuditFields(new Date(), author) },
session
)

return handlerReturn
})
logger.info(logs.end)

return sessionReturn
} catch (err) {
logger.error(err, logs.fail)
throw err
} finally {
await session.endSession()
}
}

/**
* @import { FormMetadataAuthor } from '@defra/forms-model'
* @import { ClientSession } from 'mongodb'
*/
80 changes: 80 additions & 0 deletions src/api/forms/service/callSessionTransaction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Boom from '@hapi/boom'
import { pino } from 'pino'

import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js'
import { formMetadataDocument } from '~/src/api/forms/service/__stubs__/service.js'
import { callSessionTransaction } from '~/src/api/forms/service/callSessionTransaction.js'
import { getAuthor } from '~/src/helpers/get-author.js'
import { prepareDb } from '~/src/mongo.js'

jest.mock('~/src/helpers/get-author.js')
jest.mock('~/src/api/forms/repositories/form-metadata-repository.js')
jest.mock('~/src/mongo.js')

jest.useFakeTimers().setSystemTime(new Date('2020-01-01'))
describe('lists', () => {
beforeAll(async () => {
await prepareDb(pino())
})

beforeEach(() => {
jest.mocked(formMetadata.get).mockResolvedValue(formMetadataDocument)
})

describe('callSessionTransaction', () => {
const id = '661e4ca5039739ef2902b214'
const author = getAuthor()
const dateUsedInFakeTime = new Date('2020-01-01')
const defaultAudit = {
'draft.updatedAt': dateUsedInFakeTime,
'draft.updatedBy': author,
updatedAt: dateUsedInFakeTime,
updatedBy: author
}

beforeAll(async () => {
await prepareDb(pino())
})

it('should update the component', async () => {
const transactionResolved = '265a71fd-f2c2-4028-94aa-7c1e2739730f'
const transactionHandler = jest
.fn()
.mockResolvedValue(transactionResolved)

const dbMetadataSpy = jest.spyOn(formMetadata, 'update')

const result = await callSessionTransaction(
id,
transactionHandler,
author,
{
start: 'started',
end: 'finished',
fail: 'failed'
}
)

expect(transactionHandler).toHaveBeenCalled()
expect(result).toBe(transactionResolved)
const [metaFormId, metaUpdateOperations] = dbMetadataSpy.mock.calls[0]
expect(metaFormId).toBe(id)

expect(metaUpdateOperations.$set).toEqual(defaultAudit)
})

it('should correctly surface the error is the component is not found', async () => {
const transactionHandler = jest
.fn()
.mockRejectedValue(Boom.notFound('Not found'))

await expect(
callSessionTransaction(id, transactionHandler, author, {
start: 'started',
end: 'finished',
fail: 'failed'
})
).rejects.toThrow(Boom.notFound('Not found'))
})
})
})
38 changes: 38 additions & 0 deletions src/api/forms/service/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Boom from '@hapi/boom'
import { MongoServerError, ObjectId } from 'mongodb'
import { pino } from 'pino'

import { buildList } from '~/src/api/forms/__stubs__/definition.js'
import { InvalidFormDefinitionError } from '~/src/api/forms/errors.js'
import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js'
import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js'
Expand All @@ -21,6 +22,7 @@ import {
removeForm,
updateFormMetadata
} from '~/src/api/forms/service/index.js'
import { addListsToDraftFormDefinition } from '~/src/api/forms/service/lists.js'
import * as formTemplates from '~/src/api/forms/templates.js'
import { getAuthor } from '~/src/helpers/get-author.js'
import { prepareDb } from '~/src/mongo.js'
Expand Down Expand Up @@ -355,6 +357,42 @@ describe('Forms service', () => {
)
})
})

describe('high level tests', () => {
const defaultAudit = {
'draft.updatedAt': dateUsedInFakeTime,
'draft.updatedBy': author,
updatedAt: dateUsedInFakeTime,
updatedBy: author
}
const dbMetadataSpy = jest.spyOn(formMetadata, 'update')
const expectMetadataUpdate = () => {
expect(dbMetadataSpy).toHaveBeenCalled()
const [formId, updateFilter] = dbMetadataSpy.mock.calls[0]
expect(formId).toBe(id)
expect(updateFilter.$set).toEqual(defaultAudit)
}

describe('addListsToDraftFormDefinition', () => {
it('should add a list of lists to the form definition', async () => {
const expectedLists = [buildList()]
const addListsMock = jest
.mocked(formDefinition.addLists)
.mockResolvedValueOnce(expectedLists)

const result = await addListsToDraftFormDefinition(
id,
expectedLists,
author
)
const [expectedFormId, listToInsert] = addListsMock.mock.calls[0]
expect(expectedFormId).toBe(id)
expect(listToInsert).toEqual(expectedLists)
expect(result).toEqual(expectedLists)
expectMetadataUpdate()
})
})
})
})

/**
Expand Down
149 changes: 30 additions & 119 deletions src/api/forms/service/lists.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as formDefinition from '~/src/api/forms/repositories/form-definition-repository.js'
import * as formMetadata from '~/src/api/forms/repositories/form-metadata-repository.js'
import { logger, partialAuditFields } from '~/src/api/forms/service/shared.js'
import { client } from '~/src/mongo.js'
import { callSessionTransaction } from '~/src/api/forms/service/callSessionTransaction.js'

/**
* Add a list of new lists to the draft form definition
Expand All @@ -10,48 +8,17 @@ import { client } from '~/src/mongo.js'
* @param {FormMetadataAuthor} author
*/
export async function addListsToDraftFormDefinition(formId, lists, author) {
logger.info(
`Adding lists ${lists.map((list) => list.name).join(', ')} on Form Definition (draft) for form ID ${formId}`
const listStr = lists.map((list) => list.name).join(', ')
return callSessionTransaction(
formId,
(session) => formDefinition.addLists(formId, lists, session),
author,
{
start: `Adding lists ${listStr} on Form Definition (draft) for form ID ${formId}`,
end: `Added lists ${listStr} on Form Definition (draft) for form ID ${formId}`,
fail: `Failed to add lists ${listStr} on Form Definition (draft) for form ID ${formId}`
}
)

const session = client.startSession()

try {
const newForm = await session.withTransaction(async () => {
// Add the lists to the form definition
const returnedLists = await formDefinition.addLists(
formId,
lists,
session
)

const now = new Date()
await formMetadata.update(
formId,
{
$set: partialAuditFields(now, author)
},
session
)

return returnedLists
})

logger.info(
`Added lists ${lists.map((list) => list.name).join(', ')} on Form Definition (draft) for form ID ${formId}`
)

return newForm
} catch (err) {
logger.error(
err,
`Failed to add lists ${lists.map((list) => list.id).join(', ')} on Form Definition (draft) for form ID ${formId}`
)

throw err
} finally {
await session.endSession()
}
}

/**
Expand All @@ -67,49 +34,16 @@ export async function updateListOnDraftFormDefinition(
list,
author
) {
logger.info(
`Updating list ${listId} on Form Definition (draft) for form ID ${formId}`
return callSessionTransaction(
formId,
(session) => formDefinition.updateList(formId, listId, list, session),
author,
{
start: `Updating list ${listId} on Form Definition (draft) for form ID ${formId}`,
end: `Updated list ${listId} on Form Definition (draft) for form ID ${formId}`,
fail: `Failed to update list ${listId} on Form Definition (draft) for form ID ${formId}`
}
)

const session = client.startSession()

try {
const updatedList = await session.withTransaction(async () => {
// Update the list on the form definition
const returnedLists = await formDefinition.updateList(
formId,
listId,
list,
session
)

const now = new Date()
await formMetadata.update(
formId,
{
$set: partialAuditFields(now, author)
},
session
)

return returnedLists
})

logger.info(
`Updated list ${listId} on Form Definition (draft) for form ID ${formId}`
)

return updatedList
} catch (err) {
logger.error(
err,
`Failed to update list ${listId} on Form Definition (draft) for form ID ${formId}`
)

throw err
} finally {
await session.endSession()
}
}

/**
Expand All @@ -119,42 +53,19 @@ export async function updateListOnDraftFormDefinition(
* @param {FormMetadataAuthor} author
*/
export async function removeListOnDraftFormDefinition(formId, listId, author) {
logger.info(
`Removing list ${listId} on Form Definition (draft) for form ID ${formId}`
await callSessionTransaction(
formId,
(session) => formDefinition.removeList(formId, listId, session),
author,
{
start: `Removing list ${listId} on Form Definition (draft) for form ID ${formId}`,
end: `Removed list ${listId} on Form Definition (draft) for form ID ${formId}`,
fail: `Failed to remove list ${listId} on Form Definition (draft) for form ID ${formId}`
}
)

const session = client.startSession()

try {
await session.withTransaction(async () => {
// Update the list on the form definition
await formDefinition.removeList(formId, listId, session)

const now = new Date()
await formMetadata.update(
formId,
{
$set: partialAuditFields(now, author)
},
session
)
})

logger.info(
`Removed list ${listId} on Form Definition (draft) for form ID ${formId}`
)
} catch (err) {
logger.error(
err,
`Failed to remove list ${listId} on Form Definition (draft) for form ID ${formId}`
)

throw err
} finally {
await session.endSession()
}
}

/**
* @import { FormMetadataAuthor, List } from '@defra/forms-model'
* @import { ClientSession } from 'mongodb'
*/
Loading
Loading