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

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from

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

}
}

let lowerSemverObj: { metadata?: string | undefined; major: number; minor: number; patch: number } = {
Copy link
Member

Choose a reason for hiding this comment

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

In a situation where two things are the exact same type, we should avoid redefining types. This allows us to have a single source of truth. If we ever updated that type for any reason, we'd only have to update it in one place and TS would guide us through updating all variables of that type.

In this scenario we should be using the SemverObject type you've already created in backend/src/models/Release.ts for both lowerSemverObj and upperSemverObj.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed as was causing bugs and didn't seem strictly necessary.

minor: NaN,
patch: NaN,
},
upperSemverObj: { metadata?: string | undefined; major: number; minor: number; patch: number } = {
Copy link
Member

Choose a reason for hiding this comment

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

Although JS allows us to define multiple variables on one line like you're doing here, I would strongly advise against it as it's simply less readable than defining multiple variables separately.

let lowerSemverObj: SemverObject = { ... }
let upperSemverObj: SemverObject = { ... }

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed above

@@ -237,9 +239,11 @@ export async function newReleaseComment(user: UserInterface, modelId: string, se
export async function getModelReleases(
user: UserInterface,
modelId: string,
q?: string,
Copy link
Member

@ARADDCC012 ARADDCC012 Nov 25, 2024

Choose a reason for hiding this comment

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

I'm never a fan of single character variable names like this. It's more confusing than simply giving it a proper name.

Consider calling this querySemver as you have later on.

Copy link
Member Author

Choose a reason for hiding this comment

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

True. Fixed

): Promise<Array<ReleaseDoc & { model: ModelInterface; files: FileInterface[] }>> {
const query = q === undefined ? { modelId } : getQuerySyntax(q, modelId)
Copy link
Member

Choose a reason for hiding this comment

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

This can be simplified to const query = getQuerySyntax(q, modelId) as you've already covered off the case of q === undefined inside of getQuerySyntax

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the check in getquerysyntax, dealing with it earlier makes more sense to me, but willing to change

const results = await Release.aggregate()
.match({ modelId })
.match(query!)
Copy link
Member

Choose a reason for hiding this comment

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

By using ! we're telling TS that this can't possibly be undefined/null. This is very rarely the correct way to manage types and is likely indicative of a flaw in your getQuerySyntax() logic.

I'd suggest removing this assertion and seeing where undefined/null are possibilities in earlier logic. You should fix it there instead of asserting here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out I didn't even need it anymore so just removed

@@ -17,6 +17,9 @@ export const getReleasesSchema = z.object({
required_error: 'Must specify model id as URL parameter',
}),
}),
query: z.object({
q: z.string().optional(),
Copy link
Member

Choose a reason for hiding this comment

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

Could you include an example and description here for the API docs please

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

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

@@ -248,7 +252,9 @@ export async function getModelReleases(
const model = await getModelById(user, modelId)

const auths = await authorisation.releases(user, model, results, ReleaseAction.View)
return results.filter((_, i) => auths[i].success)
return results
Copy link
Member

@ARADDCC012 ARADDCC012 Nov 25, 2024

Choose a reason for hiding this comment

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

If we care about performance here, we can combine this filter and map into a single reduce:

return results.reduce<(ReleaseInterface & { model: ModelInterface })[]>((updatedResults, result, index) => {
  if (auths[index].success) {
    updatedResults.push({ ...result, semver: semverObjectToString(result.semver) })
  }
  return updatedResults
}, [])

Copy link
Member Author

Choose a reason for hiding this comment

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

Added, but wasn't sure on how to implement the updatedResults as I can't push to it as its not an array, etc

@@ -276,11 +282,36 @@ export async function getReleasesForExport(user: UserInterface, modelId: string,
return releases
}

export function semverStringToObject(semver: string) {
Copy link
Member

@ARADDCC012 ARADDCC012 Nov 25, 2024

Choose a reason for hiding this comment

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

We can give this an explicit return type to ensure it's not broken by accident:

export function semverStringToObject(semver: string): SemverObject { ...

You could do the same for semverObjectToString, but that's less of a concern as it only has to return a string instead of an object of a defined shape.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added

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

for (const fileId of release.fileIds) {
let file
let file: Document<unknown, any, FileInterface> &
Copy link
Member

Choose a reason for hiding this comment

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

This can be simplified to let file: FileInterfaceDoc | 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.

Don't know why its saying I've done this, but I have changed anyway!

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