From e01cdd2f7e8e7052c545d27b2b10a8498f090825 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Wed, 29 Nov 2023 22:58:08 +0000 Subject: [PATCH] Add support for partial PIS matches by skipping first n stops Signed-off-by: Fred Boniface --- package.json | 2 +- src/controllers/pis.controllers.js | 6 +- src/middlewares/auth.middlewares.js | 4 +- src/services/pis.services.ts | 137 +++++++++++++++------------- src/utils/pis.utils.ts | 102 +++++++++++++++++++++ 5 files changed, 180 insertions(+), 71 deletions(-) create mode 100644 src/utils/pis.utils.ts diff --git a/package.json b/package.json index 1174651..dec7acd 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "zlib": "^1.0.5" }, "devDependencies": { - "@owlboard/ts-types": "^0.1.0", + "@owlboard/ts-types": "^0.1.2", "@types/jest": "^29.5.3", "eslint": "^8.39.0", "jest": "^29.6.2", diff --git a/src/controllers/pis.controllers.js b/src/controllers/pis.controllers.js index ad109a3..f83970b 100644 --- a/src/controllers/pis.controllers.js +++ b/src/controllers/pis.controllers.js @@ -48,13 +48,13 @@ async function random(req, res, next) { } async function testingTiplocArray(req, res, next) { - let array = JSON.parse(req.params.array) - return pis.findByTiplocArray(array) + let array = JSON.parse(req.params.array); + res.json(await pis.findByTiplocArray(array)); } module.exports = { byStartEndCRS, byCode, random, - testingTiplocArray + testingTiplocArray, }; diff --git a/src/middlewares/auth.middlewares.js b/src/middlewares/auth.middlewares.js index 2e524ac..77fdf3c 100644 --- a/src/middlewares/auth.middlewares.js +++ b/src/middlewares/auth.middlewares.js @@ -5,9 +5,9 @@ module.exports = async function authCheck(req, res, next) { //log.out("authMiddlewares: Checking authentication", "dbug"); logger.logger.debug("Auth check starting"); if (process.env.NODE_ENV === "development") { - logger.logger.warn("DEVELOPMENT MODE - AUTHENTICATION DISABLED") + logger.logger.warn("DEVELOPMENT MODE - AUTHENTICATION DISABLED"); res.isAuthed = true; - return next() + return next(); } try { var uuid = req.headers.uuid; diff --git a/src/services/pis.services.ts b/src/services/pis.services.ts index f5c4365..f4bbf9c 100644 --- a/src/services/pis.services.ts +++ b/src/services/pis.services.ts @@ -3,8 +3,15 @@ const db = require("../services/dbAccess.services"); const clean = require("../utils/sanitizer.utils"); +import type { OB_Pis_SimpleObject } from "@owlboard/ts-types"; + import { logger } from "../utils/logger.utils"; import { queryAggregate } from "./dbAccess.services"; +import { + getPartialEndTiplocMatchPipeline, + getFullTiplocMatchPipeline, +} from "../utils/pis.utils"; +import { Document } from "mongodb"; const supported = ["GW", "UK"]; @@ -33,7 +40,9 @@ async function findPisByOrigDest(start: string, end: string) { return search; } -async function findPisByCode(code: string) { +async function findPisByCode( + code: string +): Promise { logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`); const cleanCode = clean.removeNonNumeric(code); const query = { @@ -43,75 +52,73 @@ async function findPisByCode(code: string) { return await search; } -export async function findByTiplocArray(tiplocArray: string[]) { - // If any of the TIPLOCS in an array is RDG4AB - Replace it with READING. - // ^^ Double check those though before doing it. - const query = { - tiplocs: tiplocArray, - }; - const res = await db.query("pis", query); - // If res is empty then try to find partial match - if (res.length) { - return res; - } else { - let partial = await findPartialMatchByTiploc(tiplocArray); - return partial; +// Queries the database for PIS codes that match the given TIPLOC array +export async function findByTiplocArray( + tiplocArray: string[] +): Promise { + // Firstly fix errant TIPLOCS such as RDNG4AB which will never return a result + // currently only works with RDNG4AB - checks for presence of RDNG4AB first to + // avoid iterating over every array ever searched for. + if (tiplocArray.includes("RDNG4AB")) { + fixErrantTiplocs(tiplocArray); + } + + // PERFORMANCE NOTE: + // The majority of queries will return a full match, + // this means that a more performant pipeline is used + // to find a full match and only then are more + // complicated pipelines used to find partial matches + // if the first pipeline returns nothing. + try { + const exactMatch = await findExactMatchByTiploc(tiplocArray); + if (exactMatch) { + return convertDocument(exactMatch, "none"); + } else { + const partialEnd = await findPartialEndMatchByTiploc(tiplocArray); + if (partialEnd) { + return convertDocument(partialEnd, "first"); + } else { + // Here, I should search for a partialStart match. For now return null. + return null; + } + } + } catch (err) { + logger.error(err, "Error in findByTiplocArray"); + return null; } } -async function findPartialMatchByTiploc(tiplocArray: string[]) { - // Do some magic here, similar to findPisByOrigDest but - // ensuring that the stops in the array match the last - // x number of items in the array. +// Uses a pipeline to find an exact match +async function findExactMatchByTiploc(array: string[]): Promise { + const pipeline = getFullTiplocMatchPipeline(array); + const res = await queryAggregate("pis", pipeline); + return res[0]; +} - const pipeline = [ - { - $addFields: { - reversedTiplocs: { - $reverseArray: "$tiplocs", - }, - reversedQueryArray: { - $reverseArray: tiplocArray, - }, - }, - }, - { - $match: { - $expr: { - $gt: [ - { - $indexOfArray: [ - "$reversedTiplocs", - { - $arrayElemAt: ["$reversedQueryArray", 0], - } - ], - }, - -1, - ], - }, - }, - }, - { - $project: { - code: 1, - skipStops: { - $subtract: [ - { - $size: "$tiplocs", - }, - { - $size: tiplocArray, - }, - ], - }, - }, - }, - ]; +// Uses a pipeline to find a partial match - only supporting +// codes ending with the correct stops for now. +async function findPartialEndMatchByTiploc(array: string[]): Promise { + const pipeline = getPartialEndTiplocMatchPipeline(array); + const res = await queryAggregate("pis", pipeline); + return res[0]; +} - const res = await queryAggregate('pis', pipeline) - console.log(JSON.stringify(res)) - return res +function convertDocument(doc: Document, skipType: string): OB_Pis_SimpleObject { + return { + code: doc.code, + toc: doc.toc, + skipCount: doc.skipStops, + skipType: skipType, + }; +} + +// Changes any instance of 'RDNG4AB' to 'RDNGSTN' +function fixErrantTiplocs(input: string[]): void { + input.forEach((value, index, array) => { + if (value === "RDNG4AB") { + array[index] = "RDNGSTN"; + } + }); } module.exports = { diff --git a/src/utils/pis.utils.ts b/src/utils/pis.utils.ts new file mode 100644 index 0000000..5d46ba9 --- /dev/null +++ b/src/utils/pis.utils.ts @@ -0,0 +1,102 @@ +export function getPartialEndTiplocMatchPipeline(query: string[]) { + return [ + { + $match: { + tiplocs: { + $all: query, + }, + }, + }, + { + $addFields: { + reversedTiplocs: { + $reverseArray: "$tiplocs", + }, + query: { + $literal: query, + }, + }, + }, + { + $addFields: { + reversedQuery: { + $reverseArray: "$query", + }, + }, + }, + { + $match: { + $expr: { + $eq: [ + { + $slice: [ + "$reversedTiplocs", + 0, + { + $size: "$reversedQuery", + }, + ], + }, + "$reversedQuery", + ], + }, + }, + }, + { + $addFields: { + skipStops: { + $subtract: [ + { + $size: "$tiplocs", + }, + { + $size: "$reversedQuery", + }, + ], + }, + }, + }, + { + $sort: { + skipStops: 1, + }, + }, + { + $limit: 1, + }, + { + $project: { + code: 1, + skipStops: 1, + toc: 1, + _id: 0, + }, + }, + ]; +} + +export function getFullTiplocMatchPipeline(query: string[]) { + return [ + { + $match: { + tiplocs: query, + }, + }, + { + $limit: 1, + }, + { + $addFields: { + skipStops: 0, + }, + }, + { + $project: { + code: 1, + toc: 1, + skipStops: 1, + _id: 0, + }, + }, + ]; +}