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

Feature: Aggregate negative guidance tags for org summaries #5799

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9aaa4e4
add negative guidance tag counts to summaries
lcampbell2 Aug 19, 2024
48e3a8f
add 'negative_tags' object to org summary
lcampbell2 Aug 19, 2024
8443d10
add general 'guidanceTag' collection to db
lcampbell2 Aug 19, 2024
00d24d3
add general guidance tag loader
lcampbell2 Aug 19, 2024
9ae30c7
initialize loader
lcampbell2 Aug 19, 2024
75012ad
replace guidance tag loaders with new general loader
lcampbell2 Aug 19, 2024
b694556
fix aggregate tag tests
lcampbell2 Aug 19, 2024
892316a
add 'guidanceTags' collection to lists
lcampbell2 Aug 19, 2024
bd1baa9
Merge branch 'master' into feature/aggregate-guidance-tags-for-org-su…
lcampbell2 Aug 20, 2024
f6d53c2
Merge branch 'merge-guidance-tag-db-collections' into feature/aggrega…
lcampbell2 Aug 20, 2024
7eb6422
Merge branch 'master' into feature/aggregate-guidance-tags-for-org-su…
lcampbell2 Aug 21, 2024
7a1056e
add aggregated negative tag summaries to organization object
lcampbell2 Aug 21, 2024
dea2861
fix logs in loader
lcampbell2 Aug 22, 2024
50a8de7
add gql query to frontend
lcampbell2 Aug 22, 2024
d6d6f2f
add paginated list of aggregated negative guidance tags to org detail…
lcampbell2 Aug 22, 2024
28863b7
change size of guidance tag summary header
lcampbell2 Aug 23, 2024
5353f6f
fix conditional domain count render on GuidanceTagDetails
lcampbell2 Aug 26, 2024
d86bcf4
conditionally render tagId on GuidanceTagDetails
lcampbell2 Aug 26, 2024
2f4114d
Merge branch 'master' into feature/aggregate-guidance-tags-for-org-su…
lcampbell2 Oct 4, 2024
2060324
Merge branch 'master' into feature/aggregate-guidance-tags-for-org-su…
lcampbell2 Oct 9, 2024
c559068
fix pagination caching
lcampbell2 Oct 10, 2024
aba7544
translations
lcampbell2 Oct 10, 2024
172e2a2
add top margin
lcampbell2 Oct 10, 2024
3219f4c
change title
lcampbell2 Oct 10, 2024
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
6 changes: 5 additions & 1 deletion api/src/enums/guidance-tag-order-field.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GraphQLEnumType} from 'graphql'
import { GraphQLEnumType } from 'graphql'

export const GuidanceTagOrderField = new GraphQLEnumType({
name: 'GuidanceTagOrderField',
Expand All @@ -16,5 +16,9 @@ export const GuidanceTagOrderField = new GraphQLEnumType({
value: 'guidance',
description: 'Order guidance tag edges by tag guidance.',
},
TAG_COUNT: {
value: 'tag-count',
description: 'Order guidance tag edges by tag count.',
},
},
})
1 change: 1 addition & 0 deletions api/src/guidance-tag/loaders/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './load-spf-guidance-tags-connections'
export * from './load-ssl-guidance-tags'
export * from './load-ssl-guidance-tags-connections'
export * from './load-guidance-tags'
export * from './load-guidance-tags-connections'
307 changes: 307 additions & 0 deletions api/src/guidance-tag/loaders/load-guidance-tags-connections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import { aql } from 'arangojs'
import { fromGlobalId, toGlobalId } from 'graphql-relay'
import { t } from '@lingui/macro'

export const loadGuidanceTagSummaryConnectionsByTagId =
({ query, userKey, cleanseInput, i18n, language }) =>
async ({ guidanceTags, after, before, first, last, orderBy }) => {
const tagIds = Object.keys(guidanceTags)

let afterTemplate = aql``
let afterVar = aql``
if (typeof after !== 'undefined') {
const { id: afterId } = fromGlobalId(cleanseInput(after))
if (typeof orderBy === 'undefined') {
afterTemplate = aql`FILTER TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) > TO_NUMBER(REGEX_SPLIT(${afterId}, "[a-z]+")[1])`
} else {
let afterTemplateDirection
if (orderBy.direction === 'ASC') {
afterTemplateDirection = aql`>`
} else {
afterTemplateDirection = aql`<`
}

afterVar = aql`LET afterVar = DOCUMENT(guidanceTags, ${afterId})`

let tagField, documentField
/* istanbul ignore else */
if (orderBy.field === 'tag-id') {
tagField = aql`tag._key`
documentField = aql`afterVar._key`
} else if (orderBy.field === 'tag-name') {
tagField = aql`TRANSLATE(${language}, tag).tagName`
documentField = aql`TRANSLATE(${language}, afterVar).tagName`
} else if (orderBy.field === 'guidance') {
tagField = aql`TRANSLATE(${language}, tag).guidance`
documentField = aql`TRANSLATE(${language}, afterVar).guidance`
} else if (orderBy.field === 'tag-count') {
tagField = aql`TRANSLATE(tag._key, tagSummaries)`
documentField = aql`TRANSLATE(afterVar._key, tagSummaries)`
}

afterTemplate = aql`
FILTER ${tagField} ${afterTemplateDirection} ${documentField}
OR (${tagField} == ${documentField}
AND TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) > TO_NUMBER(REGEX_SPLIT(${afterId}, "[a-z]+")[1]))
`
}
}

let beforeTemplate = aql``
let beforeVar = aql``
if (typeof before !== 'undefined') {
const { id: beforeId } = fromGlobalId(cleanseInput(before))
if (typeof orderBy === 'undefined') {
beforeTemplate = aql`FILTER TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) < TO_NUMBER(REGEX_SPLIT(${beforeId}, "[a-z]+")[1])`
} else {
let beforeTemplateDirection
if (orderBy.direction === 'ASC') {
beforeTemplateDirection = aql`<`
} else {
beforeTemplateDirection = aql`>`
}

beforeVar = aql`LET beforeVar = DOCUMENT(guidanceTags, ${beforeId})`

let tagField, documentField
/* istanbul ignore else */
if (orderBy.field === 'tag-id') {
tagField = aql`tag._key`
documentField = aql`beforeVar._key`
} else if (orderBy.field === 'tag-name') {
tagField = aql`TRANSLATE(${language}, tag).tagName`
documentField = aql`TRANSLATE(${language}, beforeVar).tagName`
} else if (orderBy.field === 'guidance') {
tagField = aql`TRANSLATE(${language}, tag).guidance`
documentField = aql`TRANSLATE(${language}, beforeVar).guidance`
} else if (orderBy.field === 'tag-count') {
tagField = aql`TRANSLATE(tag._key, tagSummaries)`
documentField = aql`TRANSLATE(beforeVar._key, tagSummaries)`
}

beforeTemplate = aql`
FILTER ${tagField} ${beforeTemplateDirection} ${documentField}
OR (${tagField} == ${documentField}
AND TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) < TO_NUMBER(REGEX_SPLIT(${beforeId}, "[a-z]+")[1]))
`
}
}

let limitTemplate = aql``
if (typeof first === 'undefined' && typeof last === 'undefined') {
console.warn(
`User: ${userKey} did not have either \`first\` or \`last\` arguments set for: loadGuidanceTagConnectionsByTagId.`,
)
throw new Error(
i18n._(t`You must provide a \`first\` or \`last\` value to properly paginate the \`GuidanceTag\` connection.`),
)
} else if (typeof first !== 'undefined' && typeof last !== 'undefined') {
console.warn(
`User: ${userKey} attempted to have \`first\` and \`last\` arguments set for: loadGuidanceTagConnectionsByTagId.`,
)
throw new Error(
i18n._(t`Passing both \`first\` and \`last\` to paginate the \`GuidanceTag\` connection is not supported.`),
)
} else if (typeof first === 'number' || typeof last === 'number') {
/* istanbul ignore else */
if (first < 0 || last < 0) {
const argSet = typeof first !== 'undefined' ? 'first' : 'last'
console.warn(
`User: ${userKey} attempted to have \`${argSet}\` set below zero for: loadGuidanceTagConnectionsByTagId.`,
)
throw new Error(i18n._(t`\`${argSet}\` on the \`GuidanceTag\` connection cannot be less than zero.`))
} else if (first > 100 || last > 100) {
const argSet = typeof first !== 'undefined' ? 'first' : 'last'
const amount = typeof first !== 'undefined' ? first : last
console.warn(
`User: ${userKey} attempted to have \`${argSet}\` set to ${amount} for: loadGuidanceTagConnectionsByTagId.`,
)
throw new Error(
i18n._(
t`Requesting \`${amount}\` records on the \`GuidanceTag\` connection exceeds the \`${argSet}\` limit of 100 records.`,
),
)
} else if (typeof first !== 'undefined' && typeof last === 'undefined') {
limitTemplate = aql`TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) ASC LIMIT TO_NUMBER(${first})`
} else if (typeof first === 'undefined' && typeof last !== 'undefined') {
limitTemplate = aql`TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) DESC LIMIT TO_NUMBER(${last})`
}
} else {
const argSet = typeof first !== 'undefined' ? 'first' : 'last'
const typeSet = typeof first !== 'undefined' ? typeof first : typeof last
console.warn(
`User: ${userKey} attempted to have \`${argSet}\` set as a ${typeSet} for: loadGuidanceTagConnectionsByTagId.`,
)
throw new Error(i18n._(t`\`${argSet}\` must be of type \`number\` not \`${typeSet}\`.`))
}

let hasNextPageFilter = aql`FILTER TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) > TO_NUMBER(REGEX_SPLIT(LAST(retrievedGuidanceTags)._key, "[a-z]+")[1])`
let hasPreviousPageFilter = aql`FILTER TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) < TO_NUMBER(REGEX_SPLIT(FIRST(retrievedGuidanceTags)._key, "[a-z]+")[1])`
if (typeof orderBy !== 'undefined') {
let hasNextPageDirection
let hasPreviousPageDirection
if (orderBy.direction === 'ASC') {
hasNextPageDirection = aql`>`
hasPreviousPageDirection = aql`<`
} else {
hasNextPageDirection = aql`<`
hasPreviousPageDirection = aql`>`
}

let tagField, hasNextPageDocument, hasPreviousPageDocument
/* istanbul ignore else */
if (orderBy.field === 'tag-id') {
tagField = aql`tag._key`
hasNextPageDocument = aql`LAST(retrievedGuidanceTags)._key`
hasPreviousPageDocument = aql`FIRST(retrievedGuidanceTags)._key`
} else if (orderBy.field === 'tag-name') {
tagField = aql`TRANSLATE(${language}, tag).tagName`
hasNextPageDocument = aql`LAST(retrievedGuidanceTags).tagName`
hasPreviousPageDocument = aql`FIRST(retrievedGuidanceTags).tagName`
} else if (orderBy.field === 'guidance') {
tagField = aql`TRANSLATE(${language}, tag).guidance`
hasNextPageDocument = aql`LAST(retrievedGuidanceTags).guidance`
hasPreviousPageDocument = aql`FIRST(retrievedGuidanceTags).guidance`
} else if (orderBy.field === 'tag-count') {
tagField = aql`TRANSLATE(tag._key, tagSummaries)`
hasNextPageDocument = aql`LAST(retrievedGuidanceTags).count`
hasPreviousPageDocument = aql`FIRST(retrievedGuidanceTags).count`
}

hasNextPageFilter = aql`
FILTER ${tagField} ${hasNextPageDirection} ${hasNextPageDocument}
OR (${tagField} == ${hasNextPageDocument}
AND TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) > TO_NUMBER(REGEX_SPLIT(LAST(retrievedGuidanceTags)._key, "[a-z]+")[1]))
`

hasPreviousPageFilter = aql`
FILTER ${tagField} ${hasPreviousPageDirection} ${hasPreviousPageDocument}
OR (${tagField} == ${hasPreviousPageDocument}
AND TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) < TO_NUMBER(REGEX_SPLIT(FIRST(retrievedGuidanceTags)._key, "[a-z]+")[1]))
`
}

let sortByField = aql``
if (typeof orderBy !== 'undefined') {
/* istanbul ignore else */
if (orderBy.field === 'tag-id') {
sortByField = aql`tag._key ${orderBy.direction},`
} else if (orderBy.field === 'tag-name') {
sortByField = aql`TRANSLATE(${language}, tag).tagName ${orderBy.direction},`
} else if (orderBy.field === 'guidance') {
sortByField = aql`TRANSLATE(${language}, tag).guidance ${orderBy.direction},`
} else if (orderBy.field === 'tag-count') {
sortByField = aql`TRANSLATE(tag._key, tagSummaries) ${orderBy.direction},`
}
}

let sortString
if (typeof last !== 'undefined') {
sortString = aql`DESC`
} else {
sortString = aql`ASC`
}

let guidanceTagInfoCursor
try {
guidanceTagInfoCursor = await query`
WITH guidanceTags

${afterVar}
${beforeVar}

LET tagSummaries = (${guidanceTags})

LET retrievedGuidanceTags = (
FOR tag IN guidanceTags
FILTER tag._key IN ${tagIds}
${afterTemplate}
${beforeTemplate}
SORT
${sortByField}
${limitTemplate}
RETURN MERGE(
{
_id: tag._id,
_key: tag._key,
_rev: tag._rev,
_type: "guidanceTag",
id: tag._key,
tagId: tag._key,
count: TRANSLATE(tag._key, tagSummaries)
},
TRANSLATE(${language}, tag)
)
)

LET hasNextPage = (LENGTH(
FOR tag IN guidanceTags
FILTER tag._key IN ${tagIds}
${hasNextPageFilter}
SORT ${sortByField} TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) ${sortString} LIMIT 1
RETURN tag
) > 0 ? true : false)

LET hasPreviousPage = (LENGTH(
FOR tag IN guidanceTags
FILTER tag._key IN ${tagIds}
${hasPreviousPageFilter}
SORT ${sortByField} TO_NUMBER(REGEX_SPLIT(tag._key, "[a-z]+")[1]) ${sortString} LIMIT 1
RETURN tag
) > 0 ? true : false)

RETURN {
"guidanceTags": retrievedGuidanceTags,
"totalCount": LENGTH(${tagIds}),
"hasNextPage": hasNextPage,
"hasPreviousPage": hasPreviousPage,
"startKey": FIRST(retrievedGuidanceTags)._key,
"endKey": LAST(retrievedGuidanceTags)._key
}
`
} catch (err) {
console.error(
`Database error occurred while user: ${userKey} was trying to gather guidance tags in loadGuidanceTagConnectionsByTagId, error: ${err}`,
)
throw new Error(i18n._(t`Unable to load guidance tag(s). Please try again.`))
}

let guidanceTagInfo
try {
guidanceTagInfo = await guidanceTagInfoCursor.next()
} catch (err) {
console.error(
`Cursor error occurred while user: ${userKey} was trying to gather guidance tags in loadGuidanceTagConnectionsByTagId, error: ${err}`,
)
throw new Error(i18n._(t`Unable to load guidance tag(s). Please try again.`))
}

if (guidanceTagInfo.guidanceTags.length === 0) {
return {
edges: [],
totalCount: 0,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
}
}

const edges = guidanceTagInfo.guidanceTags.map((tag) => ({
cursor: toGlobalId('guidanceTag', tag._key),
node: tag,
}))

return {
edges,
totalCount: guidanceTagInfo.totalCount,
pageInfo: {
hasNextPage: guidanceTagInfo.hasNextPage,
hasPreviousPage: guidanceTagInfo.hasPreviousPage,
startCursor: toGlobalId('guidanceTag', guidanceTagInfo.startKey),
endCursor: toGlobalId('guidanceTag', guidanceTagInfo.endKey),
},
}
}
8 changes: 4 additions & 4 deletions api/src/guidance-tag/objects/guidance-tag-connection.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {GraphQLInt} from 'graphql'
import {connectionDefinitions} from 'graphql-relay'
import { GraphQLInt } from 'graphql'
import { connectionDefinitions } from 'graphql-relay'

import {guidanceTagType} from './guidance-tag'
import { guidanceTagType } from './guidance-tag'

export const guidanceTagConnection = connectionDefinitions({
name: 'GuidanceTag',
Expand All @@ -10,7 +10,7 @@ export const guidanceTagConnection = connectionDefinitions({
totalCount: {
type: GraphQLInt,
description: 'The total amount of guidance tags for a given scan type.',
resolve: ({totalCount}) => totalCount,
resolve: ({ totalCount }) => totalCount,
},
}),
})
5 changes: 5 additions & 0 deletions api/src/guidance-tag/objects/guidance-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const guidanceTagType = new GraphQLObjectType({
description: 'Links to technical information for a given tag.',
resolve: ({ refLinksTechnical }) => refLinksTechnical,
},
count: {
type: GraphQLString,
description: 'Number of times the tag has been applied.',
resolve: ({ count }) => count,
},
}),
interfaces: [nodeInterface],
})
8 changes: 8 additions & 0 deletions api/src/initialize-loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
loadSslGuidanceTagByTagId,
loadSslGuidanceTagConnectionsByTagId,
loadGuidanceTagByTagId,
loadGuidanceTagSummaryConnectionsByTagId,
} from './guidance-tag/loaders'
import {
loadOrgByKey,
Expand Down Expand Up @@ -232,6 +233,13 @@ export function initializeLoaders({ query, db, userKey, i18n, language, cleanseI
i18n,
language,
}),
loadGuidanceTagSummaryConnectionsByTagId: loadGuidanceTagSummaryConnectionsByTagId({
query,
userKey,
cleanseInput,
i18n,
language,
}),
loadGuidanceTagByTagId: loadGuidanceTagByTagId({
query,
userKey,
Expand Down
Loading