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

API add filter by reason and resolution status #1344

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@fastify/autoload": "6.1.0",
"@fastify/swagger": "^9.4.2",
"@fastify/swagger-ui": "^5.2.2",
"@moj-bichard7-developers/bichard7-next-data": "^2.0.251",
"date-fns": "^4.1.0",
"fastify": "^5.2.1",
"fastify-zod-openapi": "^4.1.1",
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/services/db/postgresFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import postgres from "postgres"

export default (): postgres.Sql<{}> => {
const dbConfig = createDbConfig()
const sql = postgres(dbConfig)
const sql = postgres({
...dbConfig
// debug: (connection, query, params) => {
// console.log("SQL Query:", query)
// console.log("Parameters:", params)
// }
})

return sql
}
15 changes: 9 additions & 6 deletions packages/api/src/services/gateways/dataStoreGateways/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,22 @@ class Postgres implements DataStoreGateway {
return await fetchCase(this.postgres, caseId, this.forceIds)
}

async fetchCases(pagination: Pagination, sortOrder: SortOrder, filters: Filters): Promise<CaseDataForIndexDto[]> {
// TODO: Add Permissions
async fetchCases(
user: User,
pagination: Pagination,
sortOrder: SortOrder,
filters: Filters
): Promise<CaseDataForIndexDto[]> {
// TODO: Add filters one by one
// TODO: Reuse triggerSql for filtering
return await fetchCases(this.postgres, this.forceIds, pagination, sortOrder, filters)
return await fetchCases(this.postgres, this.forceIds, user, pagination, sortOrder, filters)
}

async fetchNotes(errorIds: number[]): Promise<Note[]> {
return await fetchNotes(this.postgres, errorIds)
}

async fetchTriggers(errorIds: number[]): Promise<Trigger[]> {
return await fetchTriggers(this.postgres, errorIds)
async fetchTriggers(errorIds: number[], filters: Filters, user: User): Promise<Trigger[]> {
return await fetchTriggers(this.postgres, errorIds, filters, user)
}

async fetchUserByUsername(username: string): Promise<User> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import type { User } from "@moj-bichard7/common/types/User"
import type postgres from "postgres"

import type { CaseDataForIndexDto } from "../../../../../types/Case"
import type { Filters, Pagination, SortOrder } from "../../../../../types/CaseIndexQuerystring"

import { ResolutionStatus, resolutionStatusCodeByText } from "../../../../../useCases/dto/convertResolutionStatus"
import { excludedTriggersAndStatusSql } from "./filters/excludedTriggersAndStatusSql"
import { generateFilters } from "./filters/generateFilters"
import { ordering } from "./filters/ordering"

export default async (
sql: postgres.Sql,
forceIds: number[],
user: User,
pagination: Pagination,
sortOrder: SortOrder,
filters: Filters
): Promise<CaseDataForIndexDto[]> => {
const offset = (pagination.pageNum - 1) * pagination.maxPerPage
const resolutionStatus = resolutionStatusCodeByText(ResolutionStatus.Unresolved) ?? 1
const visibleCourts = user.visible_courts?.split(",")

// TODO: Filter triggers here
const triggerFiltersSql = sql`
AND (elt.trigger_code NOT IN('') AND elt.status = ${resolutionStatus})
`
let visibleCourtsSql = sql`FALSE`

if (visibleCourts && visibleCourts.length > 0) {
const regex = `(${visibleCourts.map((vc) => vc + "*").join("|")})`
visibleCourtsSql = sql`el.court_code ~* ${regex}`
}

// TODO: Other filtering goes here
const filtersSql = generateFilters(sql, resolutionStatus, filters)
const filtersSql = generateFilters(sql, user, filters)

const allCasesSql = sql`
SELECT DISTINCT
Expand All @@ -47,9 +51,9 @@ export default async (
FROM
br7own.error_list el
LEFT JOIN br7own.error_list_triggers elt ON elt.error_id = el.error_id
${triggerFiltersSql}
${excludedTriggersAndStatusSql(sql, filters, user)}
WHERE
(br7own.force_code (org_for_police_filter) = ANY (${forceIds}))
(${visibleCourtsSql} OR br7own.force_code (org_for_police_filter) = ANY (${forceIds}))
${filtersSql}
) distinctAlias
`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import type { Trigger } from "@moj-bichard7/common/types/Trigger"
import type { User } from "@moj-bichard7/common/types/User"
import type postgres from "postgres"

export default async (sql: postgres.Sql, errorIds: number[]) => {
// TODO: Add trigger filtering here
const filtersSql = sql`
-- AND elt.resolved_ts IS NULL
-- AND elt.trigger_code = ANY (ARRAY['TRPS0010'])
`
import Permission from "@moj-bichard7/common/types/Permission"
import { userAccess } from "@moj-bichard7/common/utils/userPermissions"

import type { Filters } from "../../../../../types/CaseIndexQuerystring"

import { excludedTriggersAndStatusSql } from "./filters/excludedTriggersAndStatusSql"

export default async (sql: postgres.Sql, errorIds: number[], filters: Filters, user: User) => {
if (!userAccess(user)[Permission.Triggers]) {
return []
}

const triggerCodes = filters.reasonCodes?.filter((rc) => rc.startsWith("TRP")) ?? []

let includeTriggersSql = sql``

if (triggerCodes.length > 0) {
includeTriggersSql = sql`AND elt.trigger_code = ANY (${triggerCodes})`
}

const result: Trigger[] = await sql`
SELECT
Expand All @@ -15,7 +29,8 @@ export default async (sql: postgres.Sql, errorIds: number[]) => {
br7own.error_list_triggers elt
WHERE
elt.error_id = ANY (${errorIds})
${filtersSql}
${excludedTriggersAndStatusSql(sql, filters, user)}
${includeTriggersSql}
`

return result
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { User } from "@moj-bichard7/common/types/User"

import ExceptionCode from "@moj-bichard7-developers/bichard7-next-data/dist/types/ExceptionCode"
import TriggerCode from "@moj-bichard7-developers/bichard7-next-data/dist/types/TriggerCode"
import Permission from "@moj-bichard7/common/types/Permission"
import { userAccess } from "@moj-bichard7/common/utils/userPermissions"
import { every } from "lodash"

import { Reason } from "../../../../../../types/CaseIndexQuerystring"

export const reasonFilterOnlyIncludesTriggers = (reason: Reason): boolean => reason === Reason.Triggers

export const reasonFilterOnlyIncludesExceptions = (reason: Reason): boolean => reason === Reason.Exceptions

export const reasonCodesAreExceptionsOnly = (reasonCodes: string[] | undefined): boolean => {
if (reasonCodes?.length === 0) {
return false
}

return every(reasonCodes, (rc: string) => ExceptionCode[rc as keyof typeof ExceptionCode])
}

export const reasonCodesAreTriggersOnly = (reasonCodes: string[] | undefined): boolean => {
if (reasonCodes?.length === 0) {
return false
}

return every(reasonCodes, (rc: string) => TriggerCode[rc as keyof typeof TriggerCode])
}

export const shouldFilterForExceptions = (user: User, reason: Reason): boolean =>
(userAccess(user)[Permission.Exceptions] && !userAccess(user)[Permission.Triggers]) ||
(userAccess(user)[Permission.Exceptions] && reasonFilterOnlyIncludesExceptions(reason))

export const shouldFilterForTriggers = (user: User, reason: Reason): boolean =>
(userAccess(user)[Permission.Triggers] && !userAccess(user)[Permission.Exceptions]) ||
(userAccess(user)[Permission.Triggers] && reasonFilterOnlyIncludesTriggers(reason))

export const canSeeTriggersAndException = (user: User, reason: Reason): boolean =>
userAccess(user)[Permission.Exceptions] && userAccess(user)[Permission.Triggers] && reason === Reason.All
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { User } from "@moj-bichard7/common/types/User"
import type postgres from "postgres"

import type { Filters } from "../../../../../../types/CaseIndexQuerystring"

import { resolutionStatusCodeByText } from "../../../../../../useCases/dto/convertResolutionStatus"

export const excludedTriggersAndStatusSql = (sql: postgres.Sql, filters: Filters, user: User) => {
const resolutionStatus = filters.caseState ? (resolutionStatusCodeByText(filters.caseState) ?? 1) : 1
const excludedTriggers = user.excluded_triggers?.split(",") ?? [""]

return sql`
AND elt.status = ${resolutionStatus}
AND elt.trigger_code != ANY (${excludedTriggers})
`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { User } from "@moj-bichard7/common/types/User"
import type postgres from "postgres"
import type { Row } from "postgres"

import Permission from "@moj-bichard7/common/types/Permission"
import { userAccess } from "@moj-bichard7/common/utils/userPermissions"

import type { Filters } from "../../../../../../types/CaseIndexQuerystring"

import { resolutionStatusCodeByText } from "../../../../../../useCases/dto/convertResolutionStatus"
import {
canSeeTriggersAndException,
reasonCodesAreExceptionsOnly,
reasonCodesAreTriggersOnly,
reasonFilterOnlyIncludesExceptions,
reasonFilterOnlyIncludesTriggers,
shouldFilterForExceptions,
shouldFilterForTriggers
} from "./checkPermissions"

const filterIfUnresolved = (sql: postgres.Sql, user: User, filters: Filters, resolutionStatus: number) => {
const query = []

if (shouldFilterForTriggers(user, filters.reason)) {
query.push(sql`AND el.trigger_status = ${resolutionStatus}`)
} else if (shouldFilterForExceptions(user, filters.reason)) {
query.push(sql`AND el.error_status = ${resolutionStatus}`)
} else if (canSeeTriggersAndException(user, filters.reason)) {
if (filters.reasonCodes && reasonCodesAreExceptionsOnly(filters.reasonCodes)) {
query.push(sql`AND el.error_status = ${resolutionStatus}`)
} else if (filters.reasonCodes && reasonCodesAreTriggersOnly(filters.reasonCodes)) {
query.push(sql`AND el.trigger_status = ${resolutionStatus}`)
} else {
query.push(sql`
AND (el.error_status = ${resolutionStatus} OR el.trigger_status = ${resolutionStatus})
AND (el.trigger_status = ${resolutionStatus} OR el.error_status = ${resolutionStatus})
`)
}
}

if (query.length === 0) {
return sql`AND FALSE`
}

return query.map((q) => sql`${q}`)
}

const filterIfResolved = (sql: postgres.Sql, user: User, filters: Filters, resolutionStatus: number) => {
const query = []

if (shouldFilterForTriggers(user, filters.reason)) {
query.push(sql`AND el.trigger_resolved_ts IS NOT NULL`)
} else if (shouldFilterForExceptions(user, filters.reason)) {
query.push(sql`AND el.error_status = ${resolutionStatus}`)
} else if (canSeeTriggersAndException(user, filters.reason)) {
if (filters.reasonCodes && reasonCodesAreExceptionsOnly(filters.reasonCodes)) {
query.push(sql`AND el.error_status = ${resolutionStatus}`)
} else if (filters.reasonCodes && reasonCodesAreTriggersOnly(filters.reasonCodes)) {
query.push(sql`AND el.trigger_resolved_ts IS NOT NULL`)
} else {
query.push(
sql`
AND (
el.error_resolved_ts IS NOT NULL
OR el.error_status = ${resolutionStatus}
OR el.trigger_resolved_ts IS NOT NULL
)
`
)
}
}

if (filters.resolvedByUsername || !userAccess(user)[Permission.ListAllCases]) {
const username = filters.resolvedByUsername ?? user.username

if (reasonFilterOnlyIncludesTriggers(filters.reason)) {
query.push(sql`AND el.trigger_resolved_by = ${username}`)
} else if (reasonFilterOnlyIncludesExceptions(filters.reason)) {
query.push(sql`AND el.error_resolved_by = ${username}`)
} else {
query.push(
sql`
AND (
el.error_resolved_by = ${username}
OR el.trigger_resolved_by = ${username}
OR elt.resolved_by = ${username}
)
`
)
}
}

if (query.length === 0) {
return sql`AND FALSE`
}

return query.map((q) => sql`${q}`)
}

export const filterByReasonAndResolutionStatus = (
sql: postgres.Sql,
user: User,
filters: Filters
): postgres.PendingQuery<Row[]> | postgres.PendingQuery<Row[]>[] => {
const resolutionStatus = filters.caseState ? (resolutionStatusCodeByText(filters.caseState) ?? 1) : 1

if (resolutionStatus === 2) {
return filterIfResolved(sql, user, filters, resolutionStatus)
}

return filterIfUnresolved(sql, user, filters, resolutionStatus)
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import type { User } from "@moj-bichard7/common/types/User"
import type postgres from "postgres"
import type { Row } from "postgres"

import type { Filters } from "../../../../../../types/CaseIndexQuerystring"

import { filterByCourtName } from "./courtName"
import { filterByDefendantName } from "./defendantName"
import { filterByReasonAndResolutionStatus } from "./filterByReasonAndResolutionStatus"
import { filterByReasonCodes } from "./reasonCodes"
import { filterByResolvedByUsername } from "./resolvedByUsername"

export const generateFilters = (sql: postgres.Sql, user: User, filters: Filters): postgres.PendingQuery<Row[]> => {
const queries = [
filterByDefendantName(sql, filters.defendantName),
filterByCourtName(sql, filters.courtName),
filterByReasonCodes(sql, filters),
filterByResolvedByUsername(sql, filters),
filterByReasonAndResolutionStatus(sql, user, filters)
]

export const generateFilters = (
sql: postgres.Sql,
resolutionStatus: number,
filters: Filters
): postgres.PendingQuery<Row[]> => {
return sql`
-- This makes it fast
AND (el.error_status = ${resolutionStatus} OR el.trigger_status = ${resolutionStatus})
AND (el.trigger_status = ${resolutionStatus} OR el.error_status = ${resolutionStatus})
-- End of fast
AND el.resolution_ts IS NULL
${filterByDefendantName(sql, filters.defendantName)}
${filterByCourtName(sql, filters.courtName)}
${queries.map((q) => sql`${q}`)}
`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type postgres from "postgres"

import type { Filters } from "../../../../../../types/CaseIndexQuerystring"

export const filterByReasonCodes = (sql: postgres.Sql, filters: Filters) => {
if (filters.reasonCodes === undefined || filters.reasonCodes.length === 0) {
return sql``
}

const triggerCodes = filters.reasonCodes?.filter((rc) => rc.startsWith("TRP")) ?? [""]
const exceptionCodes = filters.reasonCodes?.filter((rc) => rc.startsWith("HO")).map((rc) => `%${rc}%`) ?? [""]

return sql`
AND (
elt.trigger_code ILIKE ANY(${triggerCodes})
OR el.error_report ILIKE ANY(${exceptionCodes})
)
`
}
Loading