Skip to content
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

Bai 1111 allow semver matching and latest semver for releases #1582

Merged

Conversation

IR96334
Copy link
Member

@IR96334 IR96334 commented Oct 23, 2024

WIP PR for advanced semver range queries as stated in https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004

backend/src/services/release.ts Fixed Show resolved Hide resolved
backend/src/services/release.ts Outdated Show resolved Hide resolved
{
$or: [
{
'semver.major': { $lte: upperSemverObj.major },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If upperSemverObj doesn't exist, this part of both queries will break

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks added for upperSemver, etc

upperSemverObj = semverStringToObject(upperSemverTrimmed)
}

if (inclusivity)
Copy link
Member

@JR40159 JR40159 Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is a lot simpler and more readable, good job!
To improve upon this a bit more I would move all the logic needed to convert the semverRangeStandardised content into a form that can be used to generate this mongo query into a single function. At the moment this function contains multiple variable variables and function calls to convert the various semver formats when all you really need at this point is just the lowerSemverObj and upperSemverObj. Also your exclusivity logic doesn't currently work as it doesn't allow for half the semver query being inclusive.

Once you've got your semverRangeStandardised and checked it's not null you could capture all the logic up to this point of the mongo query in a single function, something like:

function parseSemverQuery(semverRangeStandardised: string): {
  upper: { major: string; minor: string; patch: string; inclusive: boolean }
  lower: { major: string; minor: string; patch: string; inclusive: boolean }
} {
  //parse semverRangeStandardised 
}

Then to reduce duplication in the inclusivity logic I would use if statements to set either $gte or $gt and $lse or $ls rather than repeat all the other bits of the mongo query in all the conditions. Eg:

if (semverQuery.upper.inclusive) {
  lowerComparator = '$gte' 
} else {
  lowerComparator = '$gt' 
}
if (semverQuery.lower.inclusive) {
  uppercomparator = '$lse' 
} else {
  uppercomparator = '$ls' 
}

Then you should be able to use this two variable in the relevant places in your single mongo query object Eg 'semver.major': { [lowerComparator]: semverQuery.lower.major }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes to semverRangeStandardised added, also added dynamic query stuff for inclusivity.

backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
@IR96334 IR96334 requested a review from JR40159 November 22, 2024 14:57
backend/src/services/release.ts Fixed Show fixed Hide fixed
backend/src/services/release.ts Fixed Show fixed Hide fixed
upperSemver: string = '',
lowerInclusivity = false,
upperInclusivity = false
if (expressionA.includes('>')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an if statement becomes this complex it's always worth seeing if it could be simplified.
Have you considered:

const lowerInclusivity = expressionA.includes('>=')
const upperInclusivity = expressionB.includes('<=') | expressionA.includes('<=')
const lowerSemver = expressionA.replace(/[<>=]/g, '')
const upperSemver = expressionB.replace(/[<>=]/g, '') 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, still required some if statements as expressionA can be the upperSemver as the lower/upper parts of the variables state wether they are upper or lower bounds, not first or second in query

@IR96334 IR96334 marked this pull request as ready for review December 2, 2024 10:09
)[]
}

export function getQuerySyntax(querySemver: string, modelID: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to export this? To make it clear what this function is doing, could we change the name to something like convertSemverQueryToMongoQuery()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the name. I have exported as I test it in release.spec.ts test file.

@@ -295,6 +335,142 @@ export async function getReleaseBySemver(user: UserInterface, modelId: string, s
return release
}

function getSemverQueryBounds(querySemver: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change this to something like parseSemverQuery()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

const semverRangeStandardised = semver.validRange(querySemver, { includePrerelease: false })

if (!semverRangeStandardised) {
throw BadReq(`Semver range, '${querySemver}' is invalid.`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general with errors, we try and avoid injecting variables into the messages. Instead you can pass in a context object containing any relevant data. Eg throw BadReq(Semver range is invalid., {semverQuery: querySemver})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

//If lower semver
if (expressionA.includes('>')) {
lowerInclusivity = expressionA.includes('>=')
lowerSemver = expressionA.replace(/[<>=]/g, '')
Copy link
Member

@JR40159 JR40159 Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having multiple variables for the upper and lower semvers and another set of if statements after this one, could you include .replace(/[<>=]/g, '') at the start of the function semverStringToObject() and call semverStringToObject() here? It would also mean you'd only need to have this replace in one place

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I still believe I need all 3 replaces as I don't know whether expressionA is lower or upper.

async () => {
const testQuery = getQuerySyntax('1.0.x', 'testModelID')
expect(testQuery).toBe({
modelId: 'testModelID',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For large expects like this one, I would use snapshots.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


expect(releaseModelMocks.match.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.sort.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.lookup.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.append.mock.calls.at(0)).toMatchSnapshot()
})

//test good - give good semver range with lower and upper bounds, returns valid query
//test fail - invalid range
test('getQuerySyntax > good'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think getQuerySyntax shouldn't be exported so here you would need to test calling getModelReleases with a semverQuery and verify that the match is what you expect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


expect(releaseModelMocks.match.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.sort.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.lookup.mock.calls.at(0)).toMatchSnapshot()
expect(releaseModelMocks.append.mock.calls.at(0)).toMatchSnapshot()
})

//test good - give good semver range with lower and upper bounds, returns valid query
//test fail - invalid range
test('getQuerySyntax > good'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think your new tests are correctly running. The whole test including the async functions needs to be included in the call to test(). As such this line should be test('getQuerySyntax > good', and 432 should have the closing bracket.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you look at the test coverage to check if there's anywhere you've missed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -56,8 +56,7 @@ async function validateRelease(user: UserInterface, model: ModelDoc, release: Re
const fileNames: Array<string> = []

for (const fileId of release.fileIds) {
let file: Document<unknown, any, FileInterface> &
Omit<FileInterface & Required<{ _id: Schema.Types.ObjectId }>, never>
let file: FileInterface | (undefined & Omit<FileInterface & Required<{ _id: Schema.Types.ObjectId }>, never>)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what's happened here again. let file: FileInterfaceDoc | undefined should be the correct type

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... fixed

upperSemver = expressionB.replace(/[<=]/g, '')
}

let lowerSemverObj, upperSemverObj
Copy link
Member

@ARADDCC012 ARADDCC012 Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you create variables without a default value TS will treat them as any, so these should both be typed. Also defining multiple values in a single line is something we should probably avoid.

let lowerSemverObj: SemverObject | undefined
let upperSemverObj: SemverObject | undefined

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah removed the typing because I wanted it to be undefined, forgetting that I can give the possible type of undefined!

upperSemver: string = '',
lowerInclusivity = false,
upperInclusivity = false
let lowerInclusivity, lowerSemver, upperInclusivity, upperSemver
Copy link
Member

@ARADDCC012 ARADDCC012 Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all any, they should be typed. If this was a JS project I wouldn't have a problem with defining multiple vars on a single line like this, but in TS we should either be providing a default value or explicitly typing it as MyType | undefined. As a result it's much more readable to define them on separate lines.

let var1: MyType | undefined
let var: MyOtherType | undefined
// etc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

if (!semver) {
return ''
}
let metadata: string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type isn't strictly true as it's undefined until you assign a value. Instead I'd suggest let metadata = ''

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

@IR96334 IR96334 requested review from ARADDCC012 and JR40159 December 3, 2024 17:23
@IR96334 IR96334 merged commit 7f494c3 into main Dec 6, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants