diff --git a/Dockerfile b/Dockerfile index 752a119..0db4800 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ RUN apk update && \ # apk add --no-cache git && \ COPY --chown=node:node package*.json ./ +COPY --chown=node:node assets ./assets USER node diff --git a/assets/filter.peggy b/assets/filter.peggy new file mode 100644 index 0000000..af39ccc --- /dev/null +++ b/assets/filter.peggy @@ -0,0 +1,38 @@ +Filter + = ComparingFilter / CombiningFilter + +Filters + = f:Filter _ fs:Filters {return [f, ...fs]} + / f:Filter {return [f]} + + +CombiningFilter + = "(" comb:Combinator _ filters:Filters ")" {return {[comb]: filters}} + +ComparingFilter + = "(" _ field:Field _ comp:Comparator _ value:String _ ")" {return {[field]: {[comp]: value}}} + +Field = + s:String { + switch(s) { + default: return s; // PLACEHOLDER_REPLACE_THIS_LINE + } + } + +String "string" + = s:[a-zA-Z0-9]+ {return s.join('')} + +Comparator "comparator" + = "<=" {return "$lte"} + / ">=" {return "$gte"} + / "!=" {return "$neq"} + / "=" {return "$eq"} + / ">" {return "$gt"} + / "<" {return "$lt"} + +Combinator "combinator" + = "|" {return "$or"} + / "&" {return "$and"} + +_ "whitespace" + = [ \t\n\r]* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bd215be..32375e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "ms": "2.1.3", "nest-winston": "1.9.4", "nestjs-joi": "1.10.0", + "peggy": "4.0.2", "point-in-polygon": "^1.1.0", "winston": "3.11.0" }, @@ -4323,6 +4324,47 @@ "npm": ">=5.0.0" } }, + "node_modules/@peggyjs/from-mem": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@peggyjs/from-mem/-/from-mem-1.2.1.tgz", + "integrity": "sha512-qh5zG8WKT36142/FqOYtpF0scRR3ZJ3H5XST1bJ/KV2FvyB5MvUB/tB9ZjihRe1iKjJD4PBOZczzwEx7hJtgMw==", + "dependencies": { + "semver": "7.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@peggyjs/from-mem/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@peggyjs/from-mem/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@peggyjs/from-mem/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12214,6 +12256,30 @@ "through": "~2.3" } }, + "node_modules/peggy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-4.0.2.tgz", + "integrity": "sha512-j4cepPgB20V7honmTAI+7U022y/n/wVi7Rbbd2QrMl2nifFECpngvA6Zhrz/JdmZ5LIBoTdkgHcDzzaA6C4ABg==", + "dependencies": { + "@peggyjs/from-mem": "1.2.1", + "commander": "^12.0.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/peggy/node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, "node_modules/picocolors": { "version": "1.0.0", "dev": true, @@ -13421,6 +13487,14 @@ "node": ">= 8" } }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "dev": true, diff --git a/package.json b/package.json index 0f4353a..97343c5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "ms": "2.1.3", "nest-winston": "1.9.4", "nestjs-joi": "1.10.0", + "peggy": "4.0.2", "point-in-polygon": "^1.1.0", "winston": "3.11.0" }, diff --git a/src/backend/pilot/pilot.controller.ts b/src/backend/pilot/pilot.controller.ts index aeab675..d4a5ee4 100644 --- a/src/backend/pilot/pilot.controller.ts +++ b/src/backend/pilot/pilot.controller.ts @@ -1,7 +1,9 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { JoiPipe } from 'nestjs-joi'; +import logger from '../logger'; + import { PilotDto, PilotCallsignValidator } from './pilot.dto'; import { PilotService } from './pilot.service'; @@ -15,8 +17,19 @@ export class PilotController { ) {} @Get('/') - async getAllPilots() { - const pilots = await this.pilotService.getAllPilots(); + async getAllPilots(@Query('filter') filter = '') { + let parsedFilter = {}; + + if (filter) { + try { + parsedFilter = this.pilotService.processFilter(filter); + } catch (error) { + throw new BadRequestException(error.message); + } + logger.debug('filter: %s - parsedFilter: %o', filter, parsedFilter); + } + + const pilots = await this.pilotService.getPilots(parsedFilter); return { count: pilots.length, diff --git a/src/backend/pilot/pilot.service.ts b/src/backend/pilot/pilot.service.ts index 2180042..204dfbe 100644 --- a/src/backend/pilot/pilot.service.ts +++ b/src/backend/pilot/pilot.service.ts @@ -1,6 +1,7 @@ import { ConflictException, Inject, Injectable, NotFoundException, forwardRef } from '@nestjs/common'; import Agenda from 'agenda'; import { FilterQuery } from 'mongoose'; +import { Parser } from 'peggy'; import { AirportService } from '../airport/airport.service'; import { CdmService } from '../cdm/cdm.service'; @@ -25,6 +26,17 @@ export class PilotService { ) { this.agenda.define('PILOT_cleanupPilots', this.cleanupPilots.bind(this)); this.agenda.every('10 minutes', 'PILOT_cleanupPilots'); + + this.parser = this.utilsService.generateFilter({ + adep: 'flightplan.adep', + ades: 'flightplan.ades', + }, true); + } + + private parser: Parser; + + processFilter(filter: string): FilterQuery { + return this.parser.parse(filter); } getPilots(filter: FilterQuery): Promise { diff --git a/src/backend/utils/utils.service.ts b/src/backend/utils/utils.service.ts index 7bcd283..419b8be 100644 --- a/src/backend/utils/utils.service.ts +++ b/src/backend/utils/utils.service.ts @@ -1,6 +1,8 @@ import crypto from 'node:crypto'; +import fs from 'node:fs'; import { Injectable } from '@nestjs/common'; +import peggy, { Parser } from 'peggy'; import logger from '../logger'; @@ -113,4 +115,50 @@ export class UtilsService { generateRandomBytes(length = 32, encoding: BufferEncoding = 'base64') { return crypto.randomBytes(length).toString(encoding); } + + generateFilter(fieldMapping = {}, disallowUndefinedFieldNames = false): Parser { + const baseFilter = fs.readFileSync('assets/filter.peggy', { encoding: 'utf8' }); + + if (Object.values(fieldMapping).length == 0 && disallowUndefinedFieldNames) { + return peggy.generate(baseFilter); + } + + const lines = baseFilter.replace(/\r\n/gm, '\n').split('\n'); + let line = -1; + let prefix = ''; + + // find line to replace + for (let i = 0; i < lines.length; i++) { + const lstring = lines[i]; + if (lstring.includes('PLACEHOLDER_REPLACE_THIS_LINE')) { + line = i; + const match = lstring.match(/([ \t]+)[\w\W\d ]*/i); + + if (match) { + prefix = match[1]; + } + + break; + } + } + + if (line == -1) { + return peggy.generate(baseFilter); + } + + const fieldsLines = Object.entries(fieldMapping) + .map(([k, v]) => `${prefix}case '${k}': return '${v}'`); + + if (disallowUndefinedFieldNames) { + fieldsLines.push(`${prefix}default: throw new Error('"' + s + '" is not an accepted field name');`); + } else { + fieldsLines.push(`${prefix}default: return s;`); + } + + lines.splice(line, 1, ...fieldsLines); + + const newFilter = lines.join('\n'); + + return peggy.generate(newFilter); + } }