Persist Google nearby places
All checks were successful
Build and deploy Backend / build (push) Successful in 26s
All checks were successful
Build and deploy Backend / build (push) Successful in 26s
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from '../prisma.js';
|
import { prisma } from '../prisma.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import type { Prisma } from '../generated/prisma/client.js';
|
||||||
|
|
||||||
const googleNearbySearchUrl =
|
const googleNearbySearchUrl =
|
||||||
'https://places.googleapis.com/v1/places:searchNearby';
|
'https://places.googleapis.com/v1/places:searchNearby';
|
||||||
@@ -34,6 +35,23 @@ type GoogleNearbyResponse = {
|
|||||||
places?: GoogleNearbyPlace[];
|
places?: GoogleNearbyPlace[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PersistableGooglePlace = {
|
||||||
|
googlePlaceId: string;
|
||||||
|
name: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaceWithRecentExperiences = Prisma.PlaceGetPayload<{
|
||||||
|
include: {
|
||||||
|
experiences: {
|
||||||
|
include: {
|
||||||
|
user: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
function serializeVoiceExperience(
|
function serializeVoiceExperience(
|
||||||
experience: Awaited<ReturnType<typeof prisma.voiceExperience.findMany>>[number],
|
experience: Awaited<ReturnType<typeof prisma.voiceExperience.findMany>>[number],
|
||||||
) {
|
) {
|
||||||
@@ -43,6 +61,13 @@ function serializeVoiceExperience(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializePlace(place: PlaceWithRecentExperiences) {
|
||||||
|
return {
|
||||||
|
...place,
|
||||||
|
experiences: place.experiences.map(serializeVoiceExperience),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function listPlaces() {
|
export async function listPlaces() {
|
||||||
const places = await prisma.place.findMany({
|
const places = await prisma.place.findMany({
|
||||||
include: {
|
include: {
|
||||||
@@ -55,10 +80,7 @@ export async function listPlaces() {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
return places.map((place) => ({
|
return places.map(serializePlace);
|
||||||
...place,
|
|
||||||
experiences: place.experiences.map(serializeVoiceExperience),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listNearbyPlaces(input: NearbyPlacesInput) {
|
export async function listNearbyPlaces(input: NearbyPlacesInput) {
|
||||||
@@ -70,7 +92,9 @@ export async function listNearbyPlaces(input: NearbyPlacesInput) {
|
|||||||
input.radiusMeters <= 0 ||
|
input.radiusMeters <= 0 ||
|
||||||
input.radiusMeters > maxNearbyRadiusMeters
|
input.radiusMeters > maxNearbyRadiusMeters
|
||||||
) {
|
) {
|
||||||
throw new Error(`Nearby place radius must be from 1 to ${maxNearbyRadiusMeters} meters.`);
|
throw new Error(
|
||||||
|
`Nearby place radius must be from 1 to ${maxNearbyRadiusMeters} meters.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (config.googlePlacesApiKey === '') {
|
if (config.googlePlacesApiKey === '') {
|
||||||
throw new Error('GOOGLE_PLACES_API_KEY is required for nearby place search.');
|
throw new Error('GOOGLE_PLACES_API_KEY is required for nearby place search.');
|
||||||
@@ -109,36 +133,10 @@ export async function listNearbyPlaces(input: NearbyPlacesInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as GoogleNearbyResponse;
|
const payload = (await response.json()) as GoogleNearbyResponse;
|
||||||
const places = payload.places ?? [];
|
const googlePlaces = parseGoogleNearbyPlaces(input, payload);
|
||||||
return places
|
await upsertGooglePlaces(googlePlaces);
|
||||||
.map((place) => {
|
|
||||||
const googlePlaceId = place.id;
|
|
||||||
const name = place.displayName?.text;
|
|
||||||
const latitude = place.location?.latitude;
|
|
||||||
const longitude = place.location?.longitude;
|
|
||||||
if (
|
|
||||||
!googlePlaceId ||
|
|
||||||
!name ||
|
|
||||||
latitude === undefined ||
|
|
||||||
longitude === undefined
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return listPersistedNearbyPlaces(input);
|
||||||
id: googlePlaceId,
|
|
||||||
googlePlaceId,
|
|
||||||
name,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
experiences: [],
|
|
||||||
distanceMeters: distanceMeters(input.latitude, input.longitude, latitude, longitude),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((place) => place !== null)
|
|
||||||
.filter((place) => place.distanceMeters <= input.radiusMeters)
|
|
||||||
.sort((a, b) => a.distanceMeters - b.distanceMeters)
|
|
||||||
.map(({ distanceMeters: _distanceMeters, ...place }) => place);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listVoiceExperiences() {
|
export async function listVoiceExperiences() {
|
||||||
@@ -174,3 +172,112 @@ function distanceMeters(
|
|||||||
function degreesToRadians(value: number) {
|
function degreesToRadians(value: number) {
|
||||||
return (value * Math.PI) / 180;
|
return (value * Math.PI) / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseGoogleNearbyPlaces(
|
||||||
|
input: NearbyPlacesInput,
|
||||||
|
payload: GoogleNearbyResponse,
|
||||||
|
) {
|
||||||
|
const placesByGoogleId = new Map<string, PersistableGooglePlace>();
|
||||||
|
for (const place of payload.places ?? []) {
|
||||||
|
const googlePlaceId = place.id;
|
||||||
|
const name = place.displayName?.text;
|
||||||
|
const latitude = place.location?.latitude;
|
||||||
|
const longitude = place.location?.longitude;
|
||||||
|
if (
|
||||||
|
!googlePlaceId ||
|
||||||
|
!name ||
|
||||||
|
latitude === undefined ||
|
||||||
|
longitude === undefined
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
distanceMeters(input.latitude, input.longitude, latitude, longitude) >
|
||||||
|
input.radiusMeters
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
placesByGoogleId.set(googlePlaceId, {
|
||||||
|
googlePlaceId,
|
||||||
|
name,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...placesByGoogleId.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertGooglePlaces(places: PersistableGooglePlace[]) {
|
||||||
|
await Promise.all(
|
||||||
|
places.map((place) =>
|
||||||
|
prisma.place.upsert({
|
||||||
|
where: { googlePlaceId: place.googlePlaceId },
|
||||||
|
create: place,
|
||||||
|
update: {
|
||||||
|
name: place.name,
|
||||||
|
latitude: place.latitude,
|
||||||
|
longitude: place.longitude,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPersistedNearbyPlaces(input: NearbyPlacesInput) {
|
||||||
|
const bounds = coordinateBounds(
|
||||||
|
input.latitude,
|
||||||
|
input.longitude,
|
||||||
|
input.radiusMeters,
|
||||||
|
);
|
||||||
|
const places = await prisma.place.findMany({
|
||||||
|
where: {
|
||||||
|
latitude: { gte: bounds.minLatitude, lte: bounds.maxLatitude },
|
||||||
|
longitude: { gte: bounds.minLongitude, lte: bounds.maxLongitude },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
experiences: {
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 10,
|
||||||
|
include: { user: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return places
|
||||||
|
.map((place) => ({
|
||||||
|
place,
|
||||||
|
distanceMeters: distanceMeters(
|
||||||
|
input.latitude,
|
||||||
|
input.longitude,
|
||||||
|
place.latitude,
|
||||||
|
place.longitude,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(({ distanceMeters }) => distanceMeters <= input.radiusMeters)
|
||||||
|
.sort((a, b) => a.distanceMeters - b.distanceMeters)
|
||||||
|
.map(({ place }) => serializePlace(place));
|
||||||
|
}
|
||||||
|
|
||||||
|
function coordinateBounds(
|
||||||
|
latitude: number,
|
||||||
|
longitude: number,
|
||||||
|
radiusMeters: number,
|
||||||
|
) {
|
||||||
|
const metersPerLatitudeDegree = 111320;
|
||||||
|
const latitudeDelta = radiusMeters / metersPerLatitudeDegree;
|
||||||
|
const longitudeScale = Math.max(
|
||||||
|
Math.cos(degreesToRadians(latitude)),
|
||||||
|
0.000001,
|
||||||
|
);
|
||||||
|
const longitudeDelta =
|
||||||
|
radiusMeters / (metersPerLatitudeDegree * longitudeScale);
|
||||||
|
|
||||||
|
return {
|
||||||
|
minLatitude: latitude - latitudeDelta,
|
||||||
|
maxLatitude: latitude + latitudeDelta,
|
||||||
|
minLongitude: longitude - longitudeDelta,
|
||||||
|
maxLongitude: longitude + longitudeDelta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user