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

Staging deployment #50

Merged
merged 6 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 1 addition & 11 deletions .github/workflows/publish-outpost-api-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ on:
jobs:
publish-outpost-api-image:
runs-on: ubuntu-latest
strategy:
matrix:
platforms: ["linux/amd64", "linux/arm64", "linux/arm64/v8"]
steps:
- name: "Checkout GitHub Action"
uses: actions/checkout@main
Expand All @@ -37,19 +34,12 @@ jobs:
echo "tag=default" >> $GITHUB_ENV
fi

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push outpost api docker image
uses: docker/build-push-action@v5
with:
context: .
tags: ghcr.io/wearefuturegov/outpost-api-service:${{ env.tag }}
file: Dockerfile.production
platforms: ${{ matrix.platforms }}
push: true
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=Outpost API Service image
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=Outpost API Service image,oci-mediatypes=false
labels: org.opencontainers.image.source=https://github.com/wearefuturegov/outpost-api-service
34 changes: 26 additions & 8 deletions __tests__/unit/lib/filters.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
const filters = require("./../../../src/lib/filters")

describe("locationGeometry", () => {
describe("filterLocation", () => {
it("should return an empty object if lat and lng are not provided", () => {
expect(filters.locationGeometry()).toEqual({})
expect(filters.filterLocation()).toEqual({})
})

it("should return a query object if lat and lng are provided", () => {
const lat = "40.7128"
const lng = "-74.0060"
const expectedQuery = {
"service_at_locations.location.geometry": {
$nearSphere: {
$geometry: {
type: "Point",
coordinates: [parseFloat(lng), parseFloat(lat)],
},
$geoWithin: {
$centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2],
},
},
}
expect(filters.locationGeometry(lat, lng)).toEqual(expectedQuery)
expect(filters.filterLocation(lat, lng, false)).toEqual(expectedQuery)
})

it("should return a different query object if lat and lng and keywordSearch are provided", () => {
const lat = "40.7128"
const lng = "-74.0060"
const expectedQuery = {
$or: [
{
"service_at_locations.location.geometry": {
$geoWithin: {
$centerSphere: [
[parseFloat(lng), parseFloat(lat)],
20 / 3963.2, // miles x 1609.34 = Distance in meters
],
},
},
},
{ "service_at_locations.location.geometry": { $exists: false } },
],
}
expect(filters.filterLocation(lat, lng, true)).toEqual(expectedQuery)
})
})

Expand Down
4 changes: 2 additions & 2 deletions __tests__/unit/v1/services/routes/get-services.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ describe("get-services", () => {
)
expect(geocode).toHaveBeenCalledWith(queryParams.location)
expect(interpreted_location).toEqual(results[0].formatted_address)
expect(lat).toBeCloseTo(parseInt(results[0].geometry.location.lat))
expect(lng).toBeCloseTo(parseInt(results[0].geometry.location.lng))
expect(lat).toBeCloseTo(parseFloat(results[0].geometry.location.lat))
expect(lng).toBeCloseTo(parseFloat(results[0].geometry.location.lng))
})

it("should return undefined if invalid location is provided", async () => {
Expand Down
87 changes: 25 additions & 62 deletions src/controllers/v1/services/routes/get-services.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ module.exports = {
const page = parseInt(queryParams.page) || 1
const keywords = queryParams.keywords
const location = queryParams.location
let lat = parseFloat(queryParams.lat) || undefined
let lng = parseFloat(queryParams.lng) || undefined
let lat = queryParams?.lat ? parseFloat(queryParams.lat) : undefined
let lng = queryParams?.lng ? parseFloat(queryParams.lng) : undefined
let directories = queryParams?.directories
? [].concat(queryParams.directories)
: []
Expand Down Expand Up @@ -55,11 +55,10 @@ module.exports = {
if (location && !(lat && lng)) {
try {
const { results } = await geocode(queryParams.location)
logger.debug(results)
if (results[0]) {
interpreted_location = results[0].formatted_address
lng = parseInt(results[0].geometry.location.lng)
lat = parseInt(results[0].geometry.location.lat)
lng = parseFloat(results[0].geometry.location.lng)
lat = parseFloat(results[0].geometry.location.lat)
}
} catch (error) {
logger.warn(error)
Expand Down Expand Up @@ -94,22 +93,9 @@ module.exports = {
let query = {}
query.$and = []

const locationInQuery =
parameters.location !== undefined ||
parameters.lat !== undefined ||
parameters.lng !== undefined
const filterKeywords = await filters.filterKeywords(
parameters.keywords,
locationInQuery
)
const filterKeywords = await filters.filterKeywords(parameters.keywords)
query = { ...filterKeywords, ...query }

const locationGeometry = filters.locationGeometry(
parameters.lat,
parameters.lng
)
query = { ...locationGeometry, ...query }

// add filtering for ages
const ages = filters.filterAges(parameters.minAge, parameters.maxAge)
query.$and.push(...ages)
Expand All @@ -124,6 +110,11 @@ module.exports = {

// add filtering
query.$and.push(
filters.filterLocation(
parameters.lat,
parameters.lng,
query?.$text ?? false
),
filters.filterDirectories(parameters.directories),
filters.filterTaxonomies(parameters.taxonomies),
filters.filterNeeds(parameters.needs),
Expand All @@ -138,37 +129,6 @@ module.exports = {
return query
},

/**
* this is done because of the $nearSphere method in locationGeometry.
* This is because The $nearSphere operator cannot be used with the
* countDocuments() method in MongoDB because countDocuments()
* uses an aggregation pipeline under the hood, and $nearSphere is not
* allowed in an aggregation pipeline.
* so as a workaround if we're using nearsphere we update the count
* query to prevent errors
* the result of nearsphere will include all services with a location
* so this query is a good substitute to get the totalElements value
* @TODO test this doesn't affect query object
* http://localhost:3001/api/v1/services?lat=51.2107714&lng=0.31105&per_page=10&suitabilities=physical-disabilities
* @param {*} query
* @returns
*/
createCountQuery: query => {
// "budget deep clone" we spread $and so can modify it for countQuery only
const countQuery = { ...query, $and: [...query.$and] }
if ("service_at_locations.location.geometry" in countQuery) {
delete countQuery["service_at_locations.location.geometry"]

countQuery["$and"].push({
"service_at_locations.location.geometry": {
$exists: true,
$ne: null,
},
})
}
return countQuery
},

/**
*
* @param {*} query
Expand All @@ -178,14 +138,6 @@ module.exports = {
*/
async executeQuery(query, perPage, page) {
const Service = db().collection("indexed_services")
const countQuery = this.createCountQuery(query)

logger.debug("query")
logger.debug(query)
logger.debug(JSON.stringify(query))
logger.debug("countQuery")
logger.debug(countQuery)
logger.debug(JSON.stringify(countQuery))

const queryProjection = query.$text
? {
Expand All @@ -196,16 +148,27 @@ module.exports = {
...projection,
}

const sort = query.$text
? {
score: { $meta: "textScore" },
updated_at: -1,
}
: {
updated_at: -1,
}

logger.debug("query")
logger.debug(query)
logger.debug(JSON.stringify(query))

const [results, count] = await Promise.all([
Service.find(query)
.project(queryProjection)
.sort(
query.$text ? { score: { $meta: "textScore" } } : { updated_at: -1 }
)
.sort(sort)
.limit(perPage)
.skip((page - 1) * perPage)
.toArray(),
Service.countDocuments(countQuery),
Service.countDocuments(query),
])

return { results, count }
Expand Down
66 changes: 33 additions & 33 deletions src/lib/filters.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
const { db } = require("../db")
const logger = require("../../utils/logger")

module.exports = {
locationGeometry: (lat, lng) => {
let query = {}
if (lat && lng) {
query["service_at_locations.location.geometry"] = {
// use this option to search within a defined area
// remember if you take out nearsphere to update the executeQuery function!
// this lets us get accurate result counts
// $geoWithin: {
// $centerSphere: [
// [parseFloat(lng), parseFloat(lat)],
// 10 / 3963.2, // 10 miles radius
// ],
// },
// but this is how its always been done so we will keep this for now
// added maxDistance to limit the search to 15 miles for efficiency
// nb if you add in $maxDistance you will need to update the createCountQuery function workaround
$nearSphere: {
$geometry: {
type: "Point",
coordinates: [parseFloat(lng), parseFloat(lat)],
filterLocation: (lat, lng, keywordSearch) => {
if (lat !== undefined && lng !== undefined) {
logger.debug(
`Looking for services near ${parseFloat(lat)}, ${parseFloat(lng)} `
)
// if the query has keyword search then we need to make sure that we return services with no location still too
if (keywordSearch) {
return {
$or: [
{
"service_at_locations.location.geometry": {
$geoWithin: {
$centerSphere: [
[parseFloat(lng), parseFloat(lat)],
20 / 3963.2, // miles x 1609.34 = Distance in meters
],
},
},
},
{ "service_at_locations.location.geometry": { $exists: false } },
],
}
} else {
return {
"service_at_locations.location.geometry": {
$geoWithin: {
$centerSphere: [[parseFloat(lng), parseFloat(lat)], 20 / 3963.2], // miles x 1609.34 = Distance in meters
},
},
// $maxDistance: 10 * 1609.34, // miles x 1609.34 = Distance in meters
},
}
}
}
return query
return {}
},

visibleNow: () => {
Expand All @@ -51,18 +59,10 @@ module.exports = {
* @param {...any} args
* @returns
*/
filterKeywords: async (keywords, locationInQuery = false) => {
filterKeywords: async keywords => {
let query = {}
if (keywords) {
if (locationInQuery) {
const Service = db().collection("indexed_services")
const docs = await Service.find({
$text: { $search: keywords },
}).toArray()
query._id = { $in: docs.map(doc => doc._id) }
} else {
query.$text = { $search: keywords }
}
query.$text = { $search: keywords }
}
return query
},
Expand Down
Loading