Skip to content

Commit

Permalink
Merge pull request #1237 from boostercloud/integration/1173
Browse files Browse the repository at this point in the history
  • Loading branch information
javiertoledo committed Nov 18, 2022
2 parents dca595c + a521837 commit e590232
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Migrate on ReadModel Find and Search",
"type": "minor",
"packageName": "@boostercloud/framework-core"
}
],
"packageName": "@boostercloud/framework-core",
"email": "[email protected]"
}
24 changes: 22 additions & 2 deletions packages/framework-core/src/booster-read-models-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { getLogger } from '@boostercloud/framework-common-helpers'
import { Booster } from './booster'
import { applyReadModelRequestBeforeFunctions } from './services/filter-helpers'
import { ReadModelSchemaMigrator } from './read-model-schema-migrator'

export class BoosterReadModelsReader {
public constructor(readonly config: BoosterConfig) {}
Expand All @@ -33,7 +34,16 @@ export class BoosterReadModelsReader {
if (!key) {
throw 'Tried to run a findById operation without providing a key. An ID is required to perform this operation.'
}
return Booster.readModel(readModelMetadata.class).findById(key.id, key.sequenceKey)
const currentReadModel = await Booster.readModel(readModelMetadata.class).findById(key.id, key.sequenceKey)
if (currentReadModel) {
const readModelName = readModelMetadata.class.name
const readModelSchemaMigrator = new ReadModelSchemaMigrator(this.config)
if (Array.isArray(currentReadModel)) {
return [await readModelSchemaMigrator.migrate(<ReadModelInterface>currentReadModel[0], readModelName)]
}
return readModelSchemaMigrator.migrate(<ReadModelInterface>currentReadModel, readModelName)
}
return currentReadModel
}

public async search(
Expand All @@ -48,13 +58,23 @@ export class BoosterReadModelsReader {
readModelRequest.currentUser
)

return Booster.readModel(readModelMetadata.class)
const readModelName = readModelMetadata.class.name
const readModels = await Booster.readModel(readModelMetadata.class)
.filter(readModelTransformedRequest.filters)
.sortBy(readModelTransformedRequest.sortBy)
.limit(readModelTransformedRequest.limit)
.afterCursor(readModelTransformedRequest.afterCursor)
.paginatedVersion(readModelTransformedRequest.paginatedVersion)
.search()

const readModelSchemaMigrator = new ReadModelSchemaMigrator(this.config)
if (Array.isArray(readModels)) {
return Promise.all(readModels.map((readModel) => readModelSchemaMigrator.migrate(readModel, readModelName)))
}
readModels.items = await Promise.all(
readModels.items.map((readModel) => readModelSchemaMigrator.migrate(readModel, readModelName))
)
return readModels
}

public async subscribe(
Expand Down
147 changes: 138 additions & 9 deletions packages/framework-core/test/booster-read-model-reader.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect } from './expect'
import {
InvalidParameterError,
UUID,
ReadModelRequestEnvelope,
BoosterConfig,
FilterFor,
GraphQLOperation,
NotFoundError,
InvalidParameterError,
NotAuthorizedError,
SubscriptionEnvelope,
FilterFor,
BoosterConfig,
NotFoundError,
ReadModelInterface,
ReadModelListResult,
ReadModelRequestEnvelope,
SubscriptionEnvelope,
UUID,
} from '@boostercloud/framework-types'
import { restore, fake, match, spy, replace } from 'sinon'
import { fake, match, replace, restore, SinonStub, spy, stub } from 'sinon'
import { BoosterReadModelsReader } from '../src/booster-read-models-reader'
import { random, internet } from 'faker'
import { internet, random } from 'faker'
import { Booster } from '../src/booster'
import { BoosterAuthorizer } from '../src/booster-authorizer'
import { ReadModelSchemaMigrator } from '../src/read-model-schema-migrator'

describe('BoosterReadModelReader', () => {
const config = new BoosterConfig('test')
Expand Down Expand Up @@ -134,14 +136,17 @@ describe('BoosterReadModelReader', () => {
})

describe('the `findById` method', () => {
let migratorStub: SinonStub
beforeEach(() => {
config.readModels['SomeReadModel'] = {
before: [],
} as any
migratorStub = stub(ReadModelSchemaMigrator.prototype, 'migrate')
})

afterEach(() => {
delete config.readModels['SomeReadModel']
migratorStub.restore()
})

it('validates and uses the searcher to find a read model by id', async () => {
Expand Down Expand Up @@ -174,6 +179,71 @@ describe('BoosterReadModelReader', () => {
expect(fakeValidateByIdRequest).to.have.been.calledOnceWith(readModelRequestEnvelope)
expect(fakeSearcher.findById).to.have.been.calledOnceWith('42')
})

it('calls migrate after find a read model by id', async () => {
const fakeValidateByIdRequest = fake()
replace(readModelReader as any, 'validateByIdRequest', fakeValidateByIdRequest)

const fakeSearcher = { findById: fake.returns(new TestReadModel()) }
replace(Booster, 'readModel', fake.returns(fakeSearcher))

const readModelRequestEnvelope = {
key: {
id: '42',
sequenceKey: {
name: 'salmon',
value: 'sammy',
},
},
class: { name: 'TestReadModel' },
className: 'TestReadModel',
currentUser: {
id: 'a user',
} as any,
version: 1,
requestID: 'my request!',
} as any

migratorStub.callsFake(async (readModel, readModelName) => readModel)

await readModelReader.findById(readModelRequestEnvelope)

expect(fakeValidateByIdRequest).to.have.been.calledOnceWith(readModelRequestEnvelope)
expect(fakeSearcher.findById).to.have.been.calledOnceWith('42')
expect(migratorStub).to.have.been.calledOnce
})

it('call migrate once the read model after find a read model by id and got an Array', async () => {
const fakeValidateByIdRequest = fake()
replace(readModelReader as any, 'validateByIdRequest', fakeValidateByIdRequest)
const fakeSearcher = { findById: fake.returns([new TestReadModel(), new TestReadModel()]) }
replace(Booster, 'readModel', fake.returns(fakeSearcher))

const readModelRequestEnvelope = {
key: {
id: '42',
sequenceKey: {
name: 'salmon',
value: 'sammy',
},
},
class: { name: 'TestReadModel' },
className: 'TestReadModel',
currentUser: {
id: 'a user',
} as any,
version: 1,
requestID: 'my request!',
} as any

migratorStub.callsFake(async (readModel, readModelName) => readModel)

await readModelReader.findById(readModelRequestEnvelope)

expect(fakeValidateByIdRequest).to.have.been.calledOnceWith(readModelRequestEnvelope)
expect(fakeSearcher.findById).to.have.been.calledOnceWith('42')
expect(migratorStub).to.have.been.calledOnce
})
})
})

Expand Down Expand Up @@ -273,17 +343,20 @@ describe('BoosterReadModelReader', () => {
}

describe('the "search" method', () => {
let migratorStub: SinonStub
beforeEach(() => {
config.readModels[TestReadModel.name] = {
class: TestReadModel,
authorizer: BoosterAuthorizer.authorizeRoles.bind(null, [UserRole]),
properties: [],
before: [],
}
migratorStub = stub(ReadModelSchemaMigrator.prototype, 'migrate')
})

afterEach(() => {
delete config.readModels[TestReadModel.name]
migratorStub.restore()
})

it('calls the provider search function and returns its results', async () => {
Expand All @@ -293,6 +366,8 @@ describe('BoosterReadModelReader', () => {

replace(Booster, 'config', config) // Needed because the function `Booster.readModel` references `this.config` from `searchFunction`

migratorStub.callsFake(async (readModel, readModelName) => readModel)

const result = await readModelReader.search(envelope)

expect(providerSearcherFunctionFake).to.have.been.calledOnceWithExactly(
Expand All @@ -307,6 +382,60 @@ describe('BoosterReadModelReader', () => {
expect(result).to.be.deep.equal(expectedReadModels)
})

it('calls migrates after search a read model with a simple array', async () => {
const expectedReadModels = [new TestReadModel(), new TestReadModel()]
const providerSearcherFunctionFake = fake.returns(expectedReadModels)
replace(config.provider.readModels, 'search', providerSearcherFunctionFake)

replace(Booster, 'config', config) // Needed because the function `Booster.readModel` references `this.config` from `searchFunction`

migratorStub.callsFake(async (readModel, readModelName) => readModel)

const result = await readModelReader.search(envelope)

expect(providerSearcherFunctionFake).to.have.been.calledOnceWithExactly(
match.any,
TestReadModel.name,
filters,
{},
undefined,
undefined,
false
)
expect(result).to.be.deep.equal(expectedReadModels)
expect(migratorStub).to.have.been.calledTwice
})

it('calls migrates once after paginated search a read model', async () => {
const expectedReadModels = [new TestReadModel(), new TestReadModel()]
const searchResult: ReadModelListResult<TestReadModel> = {
items: expectedReadModels,
count: 2,
cursor: {},
}
const providerSearcherFunctionFake = fake.returns(searchResult)
replace(config.provider.readModels, 'search', providerSearcherFunctionFake)

replace(Booster, 'config', config) // Needed because the function `Booster.readModel` references `this.config` from `searchFunction`

migratorStub.callsFake(async (readModel, readModelName) => readModel)

envelope.paginatedVersion = true
const result = await readModelReader.search(envelope)

expect(providerSearcherFunctionFake).to.have.been.calledOnceWithExactly(
match.any,
TestReadModel.name,
filters,
{},
undefined,
undefined,
true
)
expect(result).to.be.deep.equal(searchResult)
expect(migratorStub).to.have.been.calledTwice
})

context('when there is only one before hook function', () => {
const beforeFnSpy = spy(beforeFn)

Expand Down

0 comments on commit e590232

Please sign in to comment.